小言_互联网的博客

紫薇星上的Java——深入理解:线程与多线程

275人阅读  评论(0)

本文章部分观点与图片引自www.baidu.comhttps://edu.aliyun.com/

好久不见,甚是想念,我是紫郡。

这篇文章来深入的整理一下线程的知识点。


                                              Java多线程编程


                                                           进程与线程

Java语言最大的特点就是支持多线程开发,也是为数不多的支持多线程的编程语言,所以在整个Java学习中,如果不能对多线程的概念有一个全面的细致的理解,那在之后的项目中尤其是并发访问的设计中就会出现的严重的技术缺陷。

如果要想理解线程,那么就要先理解一下进程的概念,在传统的DOS系统的时代,其本身有一个特征:如果你的电脑出现了病毒,那么所有的程序将无法执行,因为传统的DOS是采用单进程处理,而单进程最大的特点就是在同一时间段内只允许一个程序在执行。

后来到了Windows的时代就开启了多进程的设计,也就是在同一个时间段上可以运行多个程序,并且这些程序会进行资源的轮流抢占,所以在同一个时间段上会有多个程序依次执行,但是在同一个时间点上只会有一个进程执行,这就是在单核CPU时的状态;而后来有了多核的CPU,由于可以处理的CPU多了,那么即便有再多的进程出现,也会比单核CPU处理的速度有所提升。

打开电脑上的任务管理器,可以看到CPU的情况以及各个进程的状态,Windows是一个绝对的多进程的系统,这么多进程一定是同时执行的。

那么什么是线程呢?线程是在进程的基础之上划分的更小的程序单元,线程是在进程基础上创建并且使用的,所以线程依赖进程的支持,但是线程的启动速度要比进程快许多,所以使用多线程进行并发处理的时候,其执行的性能要高于进程,所以我们说进程是在操作系统上的划分,而线程是对进程的划分。

Java是多线程的编程语言,所以Java在进行并发访问处理的时候可以得到更高的处理性能。


                                                           实现多线程

如果想要在Java中实现多线程的定义,那么就需要有一个专门的线程主体类进行线程的执行任务的定义,而这个主体类是有要求的,必须实现特定的接口或者继承特定的父类才可以完成。

继承Thread类实现多线程

在Java中提供有一个java.lang.Thread的程序类,那么一个类只要继承了此类就表示这个类为线程的主体类;但并不意味着这个类就可以实现多线程处理了,因为还需要覆写Thread类的run()(public void run() )方法,这个方法就属于线程的主方法。


  
  1. class MyThread extends Thread{ // 线程的主体类
  2. private String title;
  3. public MyThread(String title) {
  4. this.title = title;
  5. }
  6. @Override
  7. public void run() { //线程的主体方法
  8. for( int x = 0; x < 4; x++) {
  9. System.out.println( this.title + "x的值为:" + x);
  10. }
  11. }
  12. }

多线程要执行的功能都应该在run()方法中进行定义。需要说明的是:在正常情况下,如果想使用一个实例化对象,然后调用类中提供的方法,但是run()方法是不能够直接被调用的,因为这里面牵扯到操作系统的资源调度的问题,所以要想启动多线程,就要使用start()方法。

这里为什么不能直接调用呢,我们来举个例子,假如现在实例化了三个对象,分别执行run()方法:


  
  1. public class first {
  2. public static void main( String args[] ){
  3. new MyThread( "线程A:").run();
  4. new MyThread( "线程B:").run();
  5. new MyThread( "线程C:").run();
  6. }
  7. }

编译通过,运行结果如下:


  
  1. 线程 A :x的值为:0
  2. 线程 A :x的值为:1
  3. 线程 A :x的值为:2
  4. 线程 A :x的值为:3
  5. 线程 B :x的值为:0
  6. 线程 B :x的值为:1
  7. 线程 B :x的值为:2
  8. 线程 B :x的值为:3
  9. 线程 C :x的值为:0
  10. 线程 C :x的值为:1
  11. 线程 C :x的值为:2
  12. 线程 C :x的值为:3

可以看到A先执行完,B再执行,C再执行,这时候是顺序执行,而不是像多线程一样交互执行。我们再使用start()方法看一下:


  
  1. public class first {
  2. public static void main( String args[] ){
  3. new MyThread( "线程A:").start();
  4. new MyThread( "线程B:").start();
  5. new MyThread( "线程C:").start();
  6. }
  7. }

然后再看执行结果:


  
  1. 线程 A :x的值为:0
  2. 线程 B :x的值为:0
  3. 线程 B :x的值为:1
  4. 线程 B :x的值为:2
  5. 线程 B :x的值为:3
  6. 线程 C :x的值为:0
  7. 线程 C :x的值为:1
  8. 线程 C :x的值为:2
  9. 线程 C :x的值为:3
  10. 线程 A :x的值为:1
  11. 线程 A :x的值为:2
  12. 线程 A :x的值为:3

这个时候才能看到多线程启动了,通过此时的调用可以发现虽然使用了start()方法执行,但最后执行的还是run()方法,并且执行顺序不可控,每次执行的结果中顺序都是不一样的。

那么问题就出现了,为什么多线程的启动不使用run()方法而使用Thread中的start()方法呢?我们要在start()方法中寻找答案,我们使用Eclipse打开源代码:


  
  1. public synchronized void start() {
  2. if (threadStatus != 0)
  3. throw new IllegalThreadStateException();
  4. group.add( this);
  5. boolean started = false;
  6. try {
  7. start0();
  8. started = true;
  9. } finally {
  10. try {
  11. if (!started) {
  12. group.threadStartFailed( this);
  13. }
  14. } catch (Throwable ignore) {
  15. }
  16. }
  17. }
  18. private native void start0();

通过源代码发现在strat()中会抛出一个“IllegalThreadStateException”异常类对象,但是整个的程序并没有使用throws或者明确的try...catch处理,因为该异常一定是RuntimeException的子类,每一个线程的对象只允许启动一次,如果重复启动,就会抛出此异常,我们来看一下异常:


  
  1. public class first {
  2. public static void main( String args[] ){
  3. MyThread mt = new MyThread( "线程A:");
  4. mt.start();
  5. mt.start(); //重复进行线程启动
  6. }
  7. }

这时候再来看执行结果:


  
  1. Exception in thread " main" 线程 A :x的值为:0
  2. 线程 A :x的值为:1
  3. 线程 A :x的值为:2
  4. 线程 A :x的值为:3
  5. java .lang .IllegalThreadStateException

可以看到明显的错误:Exception in thread "main" java.lang.IllegalThreadStateException。

接下来我们可以看到在start()方法中有一个明显的start0(),还可以看到在start0()这个方法中没有参数,没有实现。但是有一个非常重要的关键字native,在Java程序的执行过程中考虑到不同层次开发者的需求,所以支持由本地的操作系统函数调用,这项技术被称为JNI(Java Native Interface)技术,但在Java开发中不推荐这样使用,利用这项技术可以使用一些操作系统提供的底层函数进行一些特殊处理,而在Thread类中提供的start0()就表示需要将此方法依赖于不同的操作系统实现。

要注意,在任何情况下只要定义了多线程,多线程的启动方案永远只有一种:Thread类中的start()方法,使用不同版本的JVM是要匹配不同的操作系统。

基于Runnable接口实现多线程

虽然可以使用Thread继承实现多线程,但是在Java中对于继承是有单继承局限的,所以在Java中还提供有第二种多线程主体定义形式:实现java.lang.Runnable接口,此接口定义如下:


  
  1. @FunctionalInterface //JDK1.8引入了Lambda表达式之后就变为了函数式接口
  2. public interface Runnable{
  3. public void run();
  4. }

我们可以直接在刚才的程序中使用Runnable接口:


  
  1. class MyThread implements Runnable{ // 线程的主体类
  2. private String title;
  3. public MyThread(String title) {
  4. this.title = title;
  5. }
  6. @Override
  7. public void run() { //线程的主体方法
  8. for( int x = 0; x < 4; x++) {
  9. System.out.println( this.title + "x的值为:" + x);
  10. }
  11. }
  12. }

但是由于不在继承Thread父类,那么对于MyThread类中也不再支持strat()方法,但是如果不使用Thread.start()是无法进行多线程启动的,这时我们就需要观察一下Thread类所提供的构造方法:


  
  1. public Thread(Runnable target) {
  2. init( null, target, "Thread-" + nextThreadNum(), 0);
  3. }

可以看到将Runnable的子类对象传递到Thread的构造方法中,再由Thread去调用start()。我们来修改一下主函数:


  
  1. public class first {
  2. public static void main( String args[] ){
  3. Thread threadA = new Thread( new MyThread( "线程A:"));
  4. Thread threadB = new Thread( new MyThread( "线程B:"));
  5. Thread threadC = new Thread( new MyThread( "线程C:"));
  6. threadA.start();
  7. threadB.start();
  8. threadC.start();
  9. }
  10. }

运行结果与上面的相同,当然因为电脑CPU太快了可能会出现一个线程已经结束了下一个还没有开始的情况,大家可以把循环数字调大一些,这样更容易看出多线程的进行:


  
  1. 线程 C :x的值为:0
  2. 线程 A :x的值为:0
  3. 线程 B :x的值为:0
  4. 线程 A :x的值为:1
  5. 线程 A :x的值为:2
  6. 线程 C :x的值为:1
  7. 线程 A :x的值为:3
  8. 线程 B :x的值为:1
  9. 线程 C :x的值为:2
  10. 线程 B :x的值为:2
  11. 线程 C :x的值为:3
  12. 线程 B :x的值为:3

这个时候我们就可以看出,由于只是实现了Runnable的接口对象,所以此时线程主体上就不会有单继承的局限了,这样的设计才是一个标准型的设计。

可以发现从JDK1.8开始,Runnable接口使用了函数式接口定义,所以也可以使用Lambda表达式进行线程类的实现:


  
  1. public class first {
  2. public static void main( String args[] ){
  3. for( int x = 0; x < 3; x ++) {
  4. String title = "线程" + x + ":";
  5. Runnable run = ()->{
  6. for( int y = 0; y < 4; y ++) {
  7. System. out.println(title + "y的值为:" + y);
  8. }
  9. };
  10. new Thread(run).start();;
  11. }
  12. }
  13. }

可以看到,结果也是交替进行输出:


  
  1. 线程1 :y的值为:0
  2. 线程1 :y的值为:1
  3. 线程1 :y的值为:2
  4. 线程2 :y的值为:0
  5. 线程0 :y的值为:0
  6. 线程2 :y的值为:1
  7. 线程1 :y的值为:3
  8. 线程2 :y的值为:2
  9. 线程0 :y的值为:1
  10. 线程2 :y的值为:3
  11. 线程0 :y的值为:2
  12. 线程0 :y的值为:3

当然这种是传统的代码形式,我们可以进行适当的简化,直接将run()的接收放在Thread中:


  
  1. for( int x = 0; x < 3; x ++) {
  2. String title = "线程" + x + ":";
  3. new Thread(()->{
  4. for( int y = 0; y < 4; y ++) {
  5. System.out.println(title + "y的值为:" + y);
  6. }
  7. }).start();
  8. }

结果依然是正常执行:


  
  1. 线程0 :y的值为:0
  2. 线程2 :y的值为:0
  3. 线程2 :y的值为:1
  4. 线程2 :y的值为:2
  5. 线程1 :y的值为:0
  6. 线程2 :y的值为:3
  7. 线程0 :y的值为:1
  8. 线程1 :y的值为:1
  9. 线程0 :y的值为:2
  10. 线程1 :y的值为:2
  11. 线程1 :y的值为:3
  12. 线程0 :y的值为:3

在以后的开发中对于多线程的实现,优先考虑Runnable接口实现,并且永远都是通过Thread对象启动多线程。

Thread与Runnable的关系

经过一系列的分析之后可以发现,在多线程的实现过程中已经有了两类做法:Thread类、Runnable类,如果从代码的结构本身来讲,使用Runnable是最方便的,既可以避免单继承的局限,又能更好的进行功能上的扩充。

但是从结构上要观察Thread和Runnable的联系,在Thread类的定义中可以看到:public class Thread implements Runnable,可以发现Thread类也是Runnable接口的子类,那么之前在覆写Thread类的时候实际上还是在覆写Runnable的run()方法,于是此时来观察一下刚才使用Runnable接口程序的类结构:


  
  1. class MyThread implements Runnable{ // 线程的主体类
  2. private String title;
  3. public MyThread(String title) {
  4. this.title = title;
  5. }
  6. @Override
  7. public void run() { //线程的主体方法
  8. for( int x = 0; x < 4; x++) {
  9. System.out.println( this.title + "x的值为:" + x);
  10. }
  11. }
  12. }
  13. public class first {
  14. public static void main( String args[] ){
  15. Thread threadA = new Thread( new MyThread( "线程A:"));
  16. Thread threadB = new Thread( new MyThread( "线程B:"));
  17. Thread threadC = new Thread( new MyThread( "线程C:"));
  18. threadA.start();
  19. threadB.start();
  20. threadC.start();
  21. }
  22. }

在这里我们解释一下它们之间的关系:Runnable接口有两个子类:Thread类和MyThread类,其中我们如果需要进行多线程处理的时候就要产生一个测试类,也就是客户端,而客户是通过Thread threadA = new Thread(new MyThread("线程A:"));这条语句来调用Thread类和MyThread类,并通过调用start()方法来启动多线程,而start()实际上是调用了run()方法,Thread类在这个结构中做的就是代理的角色,负责线程相关的资源调度;而MyThread类是一个真实业务处理类,处理的都是核心业务。

可以看到在多线程设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的思想,而所有的辅助实现全部交由Thread类来处理。

在进行Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,怎么进行的呢?我们回到刚才看到的Thread类的构造方法,可以看到一个target,在源代码中可以找到对target的定义:private Runnable target;这就说明通过Thread类的构造方法传递了一个Runnable接口对象的时候,这个对象将被Thread类中的target属性所保存。我们再从Thread的源代码中找到run()方法:


  
  1. @Override
  2. public void run() {
  3. if (target != null) {
  4. target.run();
  5. }
  6. }

看到这里有的人可能已经明白了,在start()方法执行时会调用Thread类中的run()方法,而这个run()方法会调用Runnable接口子类被覆写过的run()方法,这样就完成了一次调用实现,这就是整个的Thread和Runnable的关系。

多线程开发的本质是在于多个线程可以进行同一资源的抢占,Thread主要描述的是线程,资源主要是通过Runnable描述的。而在高并发的时候,其实就是线程实现子类实现多个线程对象去抢占资源,而实际上线程对象都通过Thread类来实例化从而进行对资源的访问:

我们可以做一个简单的案例来实现一下资源并发情况:


  
  1. class MyThread implements Runnable{ // 线程的主体类
  2. private int ticket = 5;
  3. @Override
  4. public void run() { //线程的主体方法
  5. for( int x = 0; x < 100; x++) {
  6. if( this.ticket > 0) {
  7. System.out.println( "卖票中,ticket = " + this.ticket --);
  8. }
  9. }
  10. }
  11. }
  12. public class first {
  13. public static void main( String args[] ){
  14. MyThread mt = new MyThread();
  15. new Thread(mt).start(); //第一个线程
  16. new Thread(mt).start(); //第二个线程
  17. new Thread(mt).start(); //第三个线程
  18. }
  19. }

这就是一个简易的三个窗口卖五张票的多线程状况,运行结果:


  
  1. 卖票中,ticket = 5
  2. 卖票中,ticket = 3
  3. 卖票中,ticket = 1
  4. 卖票中,ticket = 4
  5. 卖票中,ticket = 2

这就可以很清晰的看到资源抢占的过程。我们分析一下内存:首先new了一个MyThread对象mt,然后将这个对象传给了Thread的target,但是传了三个,所以这三个就会开始抢占原来mt的资源。

Callable接口实现多线程

从最传统的开发来讲如果要实现多线程那么肯定是依靠Runnable,但是Runnable接口有一个缺点:当线程执行完毕之后,我们无法获得一个返回值。所以从JDK1.5之后就提出了一个新的线程实现接口:java.util.concurrect.Callable接口,这个接口的定义如下:


  
  1. @FunctionalInterface
  2. public interface Callable<V> {
  3. V call() throws Exception;
  4. }

定义中可以看到在Callable定义的时候可以设置一个泛型,这个泛型的类型就是返回数据的类型,这样做的好处之前说过,可以避免向下转型带来的安全隐患。

但是我们要注意,Callable和Thread没有任何关系,这个先不着急,我们再从java.util.concurrect中找到一个FutureTask类:


  
  1. public class FutureTask<V> implements RunnableFuture<V>{
  2. ... ...
  3. }

这里可以看到它也有一个泛型<V>,而且实现了一个接口RunnableFuture,我们再来看这个接口:


  
  1. public interface RunnableFuture<V> extends Runnable, Future<V> {
  2. void run();
  3. }

这时候已经有点意思了,我们再来看这个接口继承的Future,在其中可以找到这样一条方法:

public V get() throws InterruptedException, ExecutionException;

刚才我们提到Callable有返回值,但是这个返回值Runnable接受不了,那么这条方法明显就是用来返回数据的。

但是我们看了这么多,还是没有找到什么和Callable有关的,不要着急,我们回到刚才的FutreTask类中,在里面仔细找找,就会发现:

private Callable<V> callable;

这样子就能看到FutreTask与Callable的联系,而FutreTask又与Thread有关,如何过这时候我们有一个线程实现类,这个类只需要实现Callable接口就可以了,所以我们的关系结构就可以画出来了:

我们来编写一下Callable实现多线程的处理:


  
  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.FutureTask;
  3. class MyThread implements Callable<String>{
  4. @Override
  5. public String call() throws Exception {
  6. for( int x = 0; x < 6; x ++) {
  7. System.out.println( "**********线程执行、x = " + x);
  8. }
  9. return "线程执行完毕!";
  10. } // 线程的主体类
  11. }
  12. public class first {
  13. public static void main( String args[] ) throws Exception{
  14. FutureTask<String> task = new FutureTask<>( new MyThread());
  15. new Thread(task).start();
  16. System.out.println( "【线程返回数据】" + task.get());
  17. }
  18. }

那么我们总结一下Runnable和Callable的区别:

Runnable是在JDK1.0的时候提出的多线程的实现接口 Callable是在JDK1.5之后提出的多线程的实现接口

java.lang.Runnable接口之中只提供有一个run()方法

并且没有返回值

java.util.concurrent.Callable接口提供有call()方法

并且提供有返回值

不管如何实现多线程,启动方式永远是java.lang.Thread中的start()方法

                                                             多线程运行状态

对于多线程开发而言,编写程序的过程中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但并不意味着调用了start()线程就已经开始运行了,因为整体的线程处理有自己的一套运行状态:

我们可以简单说明一下:

  • 任何一个线程对象都应该使用Thread类来进行封装,所以线程的启动使用的是start(),但是启动的时候若干个线程都进入到一种“就绪”的状态,也就是说现在为止并没有执行;
  • 进入就绪状态之后就需要等待进行资源调度,当某一个线程成功的调度之后进入到运行状态,也就是run()方法,但是所有的线程不可能一直持续的进行下去,所以中间需要产生一些暂停的状态,例如:某个线程执行一段时间后就需要让出资源,而后这个线程就会进入到“阻塞”状态,然后重新回归“就绪”状态;
  • 当run()方法执行完毕后,实际上该线程的主要任务就结束了,那么此时就可以直接进入到停止状态。

线程的执行要注意一点:start()是准备执行而没有执行,执行要听从操作系统的安排,所以即使是同时启动的线程A、B,也会有A先执行、B最后执行的情况,因为线程的执行情况是不可估计的。


                                                    线程的操作


                                                      线程的常用操作方法

多线程的主要操作方法都在Thread类中定义了,所以我们主要是来看一下Thread类中有哪些方法。

线程的命名和取得

我们知道多线程的运行状态是不确定的,所以在开发之中为了可以获取一些需要使用线程就只能依靠线程的名字来进行操作,所以线程的名字是一个重要的概念,在Thread类中就提供有线程名称的处理:

  • 构造方法:public Thread(Runnable target, String name);
  • 设置名字:public final synchronized void setName(String name);
  • 取得名字:public final String getName();

对于线程对象的获得是不可能只依靠一个this来完成的,因为线程的状态不可控,但有一点是明确的:所有的线程都要执行run()方法,那么这个时候就可以考虑获取当前线程,在Thread类中提供有获取当前线程的方法:

  • public static native Thread currentThread();

现在我们来观察一下线程的命名操作:


  
  1. class MyThread implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println(Thread.currentThread().getName());
  5. }
  6. }
  7. public class first {
  8. public static void main( String args[] ) throws Exception{
  9. MyThread mt = new MyThread();
  10. new Thread(mt, "线程A").start(); //设置了线程的名字
  11. new Thread(mt).start();
  12. new Thread(mt).start();
  13. new Thread(mt).start();
  14. new Thread(mt).start();
  15. new Thread(mt, "线程B").start(); //设置了线程的名字
  16. }
  17. }

这样看一下运行结果:


  
  1. 线程A
  2. Thread-2
  3. Thread-1
  4. Thread-0
  5. 线程B
  6. Thread-3

这样就能看到,但开发者为线程设置名字时就是用设置好的名字,如果没有设置那就会自动生成一个不重复的名字,怎样做的呢?我们回到刚才的构造方法:


  
  1. public Thread(ThreadGroup group, Runnable target) {
  2. init( group, target, "Thread-" + nextThreadNum(), 0);
  3. }

再找到nextThreadNum可以看到:


  
  1. private static int threadInitNumber;
  2. private static synchronized int nextThreadNum() {
  3. return threadInitNumber++;
  4. }

这样就很清楚了,是依靠了static属性来完成的。我们再来看一个程序:


  
  1. class MyThread implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println(Thread.currentThread().getName());
  5. }
  6. }
  7. public class first {
  8. public static void main( String args[] ) throws Exception{
  9. MyThread mt = new MyThread();
  10. new Thread(mt, "线程对象").start(); //设置了线程的名字
  11. mt.run(); //对象直接调用run()方法
  12. }
  13. }

这段代码最有意思的就是对象直接调用run()方法,结果如下:


  
  1. main
  2. 线程对象

通过代码可以发现使用了“mt.run();”直接在主方法中调用线程类对象中的run()方法所获得的线程对象的名字为“main”,所以可以得出结论,主方法也是一个线程。那么现在的问题来了:所有的线程都是在进程上的划分,那么进程在哪里?

每当使用Java命令执行程序的时候就表示启动了一个JVM进程,一台电脑上可以同时启动若干个JVM进程,每一个JVM进程都会有各自的线程。

在任何开发中,主线程可以创建若干个子线程处理,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程处理:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. System. out.println( "第一步!");
  4. for( int i = 0; i < Integer.MAX_VALUE; i++) {
  5. i++;
  6. }
  7. System. out.println( "第二步!");
  8. System. out.println( "第N步!");
  9. }
  10. }

如果我们有这样一个程序,很明显从第一步到第二步之间需要很长的时间,当然CPU比较好的电脑可以忽略这点时间,但是如果这是一个项目,从第一步到第二步之间耗费的时间很大程度上会影响项目,所以我们将它交给子线程来处理:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. System. out.println( "第一步!");
  4. new Thread(()->{
  5. for( int i = 0; i < Integer.MAX_VALUE; i++) {
  6. i++;
  7. }
  8. }) ;
  9. System. out.println( "第二步!");
  10. System. out.println( "第N步!");
  11. }
  12. }

这样就显示了:主线程负责处理整体流程,子线程负责处理耗时操作。就好比我们在手机上刷微博,在下拉刷新的时候并不会影响当前页面的内容显示,刷新后才会改变显示内容,也是这样的道理。

线程休眠

如果说现在希望某个线程可以暂缓执行一次,那就可以使用休眠的操作处理,在Thread类中定义有休眠方法:

  • 休眠:public static native void sleep(long millis) throws InterruptedException;
  • 休眠:public static void sleep(long millis, int nanos) throws InterruptedException;

在休眠的时候有可能产生中断异常“InterruptedException”,而中断异常属于Exception的子类,所以该异常必须进行处理:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. new Thread(()->{
  4. for( int x = 0; x < 10; x ++) {
  5. System.out.println(Thread.currentThread().getName() + "、x = " + x);
  6. try {
  7. Thread.sleep( 100);
  8. } catch (InterruptedException e) {
  9. // TODO Auto-generated catch block
  10. e.printStackTrace();
  11. }
  12. }
  13. }, "线程对象").start(); ;
  14. }
  15. }

这是一个加了休眠操作的线程处理代码,在运行时相比于没有加休眠操作的代码可以明显地看到每次线程执行后都会休眠100毫秒,然后继续执行。

这就说明了休眠的特点就是可以自动实现线程的唤醒,以便进行后续的处理。但是如果有多个线程对象,这时休眠也是有先后顺序的:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. for( int num = 0; num < 5; num ++) {
  4. new Thread(()->{
  5. for( int x = 0; x < 3; x ++) {
  6. System.out.println(Thread.currentThread().getName() + "、x = " + x);
  7. try {
  8. Thread.sleep( 100);
  9. } catch (InterruptedException e) {
  10. // TODO Auto-generated catch block
  11. e.printStackTrace();
  12. }
  13. }
  14. }, "线程对象" + num).start(); ;
  15. }
  16. }
  17. }

这段代码中我们创建了五个线程对象,每个对象执行的方法体都是一样的,那么我们来看一下结果:


  
  1. 线程对象0、x = 0
  2. 线程对象4、x = 0
  3. 线程对象3、x = 0
  4. 线程对象2、x = 0
  5. 线程对象1、x = 0
  6. 线程对象0、x = 1
  7. 线程对象2、x = 1
  8. 线程对象4、x = 1
  9. 线程对象3、x = 1
  10. 线程对象1、x = 1
  11. 线程对象4、x = 2
  12. 线程对象3、x = 2
  13. 线程对象0、x = 2
  14. 线程对象1、x = 2
  15. 线程对象2、x = 2

此时程序执行的结果看上去好像是若干个线程一起进行了休眠,然后又一起进行了自动唤醒,但实际上是有差别的。我们可以这样说:一开始有五个线程对象,它们进入run()方法的时候前后顺序是有差别的;在执行过程中中这五个线程先后进行输出也是有先后顺序的,所以我们可以看到每次的输出顺序都不一样;所以在进入休眠的时候也是有先后顺序的。我们用一张图来表示:

但是在程序执行中,这种先后顺序的区别实在是太小了,所以看起来就好像是一起休眠,一起唤醒,其实并不是。

线程中断

在之前我们了解到线程休眠时会有一个中断异常,这实际上就说明线程是可以被打断的,而这种打断肯定是由其他线程完成的。在Thread类中提供有这种中断执行的处理方法:

  • 判断线程是否被中断:public boolean isInterrupted();
  • 中断线程执行:public void interrupt();

我们来简单的举个例子来看一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Thread thread = new Thread(()->{
  4. System. out.println( "准备休眠!");
  5. try {
  6. Thread.sleep( 10000); //休眠十秒
  7. System. out.println( "休眠结束!");
  8. } catch (InterruptedException e) {
  9. // TODO Auto-generated catch block
  10. System. out.println( "休眠被打断!");
  11. }
  12. }) ;
  13. thread.start();
  14. Thread.sleep( 1000); //先休眠一秒
  15. if(!thread.isInterrupted()) {
  16. //如果没被打断
  17. System. out.println( "打断休眠!");
  18. thread.interrupt();
  19. }
  20. }
  21. }

首先我们准备一个休眠程序,要休眠十秒后被唤醒,这时如果不打断休眠的话运行结果是这样的:


  
  1. 准备休眠!
  2. 休眠结束!

第二句话隔了十秒才显示出来,那么我们看一下打断之后的结果:


  
  1. 准备休眠!
  2. 打断休眠!
  3. 休眠被打断!

打断休眠这句话只隔了一秒就显示出来了。所以说线程中断的过程与之前的线程休眠是相匹配的,所有正在执行中的线程都是可以中断的,中断线程必须进行异常的处理,所以这也是InterruptedException必须被处理的原因。

线程强制执行

所谓的线程强制执行就是指当满足于某些条件后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束。

为了理解,我们先来观察一个没有强制执行的程序:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Thread thread = new Thread(()->{
  4. for( int x = 0; x < 100; x ++) {
  5. try {
  6. Thread.sleep( 100);
  7. } catch (InterruptedException e) {
  8. // TODO Auto-generated catch block
  9. e.printStackTrace();
  10. }
  11. System. out.println(Thread.currentThread().getName() + "、x = " + x);
  12. }
  13. }, "正常执行的线程") ;
  14. thread.start();
  15. for( int x = 0; x < 100; x ++) {
  16. Thread.sleep( 100);
  17. System. out.println( "霸道的主线程:x = " + x);
  18. }
  19. }
  20. }

这个程序执行后,结果太长了就随便截取一段:

这个程序很长,因为这样我们就能看到两个线程在不断地抢占资源,交替执行一直到程序执行完毕,这时候我们如果要让主线程独占执行,那么我们就可以利用Thread类中的方法使其强制执行:

  • public final synchronized void join(long millis) throws InterruptedException;

我们来实现一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Thread mainThread = Thread.currentThread(); //获得主线程
  4. Thread thread = new Thread(()->{
  5. for( int x = 0; x < 100; x ++) {
  6. if(x == 3) {
  7. //让主线程强制执行
  8. try {
  9. mainThread. join();
  10. } catch (InterruptedException e) {
  11. // TODO Auto-generated catch block
  12. e.printStackTrace();
  13. }
  14. }
  15. try {
  16. Thread.sleep( 100);
  17. } catch (InterruptedException e) {
  18. // TODO Auto-generated catch block
  19. e.printStackTrace();
  20. }
  21. System. out.println(Thread.currentThread().getName() + "、x = " + x);
  22. }
  23. }, "正常执行的线程") ;
  24. thread.start();
  25. for( int x = 0; x < 10; x ++) {
  26. Thread.sleep( 100);
  27. System. out.println( "霸道的主线程:x = " + x);
  28. }
  29. }
  30. }

我们在线程交替执行到x = 3的时候让主线强制执行,这里为了方便查看结果我们将主线程减少到10:

这里要注意:再进行强制执行的时候一定要先获取强制执行线程对象之后才可以调用join()方法强制执行。

线程礼让

线程的礼让是指先将资源让出去给别的线程先执行,线程的礼让操作可以使用Thread类中的方法:

  •  public static native void yield();

我们在刚才的程序上实现一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Thread thread = new Thread(()->{
  4. for( int x = 0; x < 100; x ++) {
  5. if(x % 5 == 0) {
  6. Thread. yield(); //线程礼让
  7. System. out.println( "#####正常线程礼让执行!#####");
  8. }
  9. try {
  10. Thread.sleep( 100);
  11. } catch (InterruptedException e) {
  12. // TODO Auto-generated catch block
  13. e.printStackTrace();
  14. }
  15. System. out.println(Thread.currentThread().getName() + "、x = " + x);
  16. }
  17. }, "正常执行的线程") ;
  18. thread.start();
  19. for( int x = 0; x < 100; x ++) {
  20. Thread.sleep( 100);
  21. System. out.println( "霸道的主线程:x = " + x);
  22. }
  23. }
  24. }

为了更直观的看到礼让,我们在每当x = 5的时候就让正常的线程礼让一下主线程,这里依旧只截取一小部分:

当然这里我们要注意:礼让执行调用yield()方法只会礼让一次当前的资源。

线程优先级

从理论上来讲,线程的优先级越高越有可能优先执行,越有可能优先抢占到资源,但是我们要记住是理论上有可能。在Thread类中针对于优先级的操作有两个操作处理方法:

  • 设置优先级:public final void setPriority(int newPriority);
  • 获取优先级:public final int getPriority();

在进行优先级的定义的时候都是通过int型的数字来完成的,而对于此数字的选择在Thread类中就定义有三个常量:

  • 最低优先级:public static final int MIN_PRIORITY = 1;
  • 中等优先级:public static final int NORM_PRIORITY = 5;
  • 最高优先级:public static final int MAX_PRIORITY = 10;

我们来实现一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Runnable run = ()->{
  4. for( int x = 0; x < 5; x ++) {
  5. try {
  6. Thread.sleep( 1000);
  7. } catch (InterruptedException e) {
  8. // TODO Auto-generated catch block
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread().getName() + "、x = " + x);
  12. }
  13. };
  14. Thread threadA = new Thread(run, "线程对象A");
  15. Thread threadB = new Thread(run, "线程对象B");
  16. Thread threadC = new Thread(run, "线程对象C");
  17. threadA.start();
  18. threadB.start();
  19. threadC.start();
  20. }
  21. }

这时候我们没有优先级处理,所以通过休眠操作来看一下哪个线程出现早的几率高一些:


  
  1. 线程对象A、x = 0
  2. 线程对象C、x = 0
  3. 线程对象B、x = 0
  4. 线程对象A、x = 1
  5. 线程对象C、x = 1
  6. 线程对象B、x = 1
  7. 线程对象A、x = 2
  8. 线程对象C、x = 2
  9. 线程对象B、x = 2
  10. 线程对象B、x = 3
  11. 线程对象C、x = 3
  12. 线程对象A、x = 3
  13. 线程对象A、x = 4
  14. 线程对象B、x = 4
  15. 线程对象C、x = 4

随便执行一次,发现A先出来4次,B先出来1次,那么我们现在将A、B变为MIN_PRIORITY,C变为MAX_PRIORITY看一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. Runnable run = ()->{
  4. for( int x = 0; x < 5; x ++) {
  5. try {
  6. Thread.sleep( 1000);
  7. } catch (InterruptedException e) {
  8. // TODO Auto-generated catch block
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread().getName() + "、x = " + x);
  12. }
  13. };
  14. Thread threadA = new Thread(run, "线程对象A");
  15. Thread threadB = new Thread(run, "线程对象B");
  16. Thread threadC = new Thread(run, "线程对象C");
  17. threadA.setPriority(Thread.MIN_PRIORITY);
  18. threadB.setPriority(Thread.MIN_PRIORITY);
  19. threadC.setPriority(Thread.MAX_PRIORITY);
  20. threadA.start();
  21. threadB.start();
  22. threadC.start();
  23. }
  24. }

这里虽然我们提高了优先级,但之前也说了是理论上有可能会提高抢占资源的可能性,现在来看一下:


  
  1. 线程对象C、x = 0
  2. 线程对象A、x = 0
  3. 线程对象B、x = 0
  4. 线程对象A、x = 1
  5. 线程对象B、x = 1
  6. 线程对象C、x = 1
  7. 线程对象B、x = 2
  8. 线程对象A、x = 2
  9. 线程对象C、x = 2
  10. 线程对象B、x = 3
  11. 线程对象C、x = 3
  12. 线程对象A、x = 3
  13. 线程对象B、x = 4
  14. 线程对象C、x = 4
  15. 线程对象A、x = 4

虽然还是会有A、B先执行的情况,但是也有一次C先执行,这就是我们刚才说的有可能先执行但不一定绝对先执行。

那么同时也会有疑问:我们知道主方法是一个主线程,那么主线程的优先级是多少呢?我们来看一下:


  
  1. public class first {
  2. public static void main( String args[] ) throws Exception{
  3. System. out.println(Thread.currentThread().getPriority());
  4. System. out.println( new Thread().getPriority());
  5. }
  6. }

结果是:


  
  1. 5
  2. 5

也就是说主线程是属于中等优先级,而一个默认的线程对象的优先级也是中等优先级,所以在之前的程序中可以看到主线程与线程对象是交替执行的,这也说明了线程优先级高的有可能先执行但不一定绝对先执行。


                                                         线程的同步与死锁

在多线程的处理中,可以使用Runnable描述多个线程操作的资源,而Thread描述每一个线程对象,于是多个线程访问同一资源的时候如果处理不当就会产生数据的错误操作。

同步问题的引出

我们现在来简单的实现一个卖票的例子,将创建若干个线程对象实现卖票的处理操作:


  
  1. class MyThread implements Runnable{
  2. private int ticket = 10; //总票数为十张
  3. @Override
  4. public void run() {
  5. while( true) {
  6. if( this.ticket > 0) {
  7. System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket --);
  8. } else {
  9. System.out.println( "*****票卖光了*****");
  10. break;
  11. }
  12. }
  13. }
  14. }
  15. public class first {
  16. public static void main( String args[] ) throws Exception{
  17. MyThread mt = new MyThread();
  18. new Thread(mt, "票贩子A").start();
  19. new Thread(mt, "票贩子B").start();
  20. new Thread(mt, "票贩子C").start();
  21. }
  22. }

这个程序很简单易懂,创建三个线程对象来卖10张票,我们来看一下结果:


  
  1. 票贩子A卖票,ticket = 9
  2. 票贩子C卖票,ticket = 10
  3. 票贩子C卖票,ticket = 6
  4. 票贩子B卖票,ticket = 8
  5. 票贩子C卖票,ticket = 5
  6. 票贩子A卖票,ticket = 7
  7. 票贩子C卖票,ticket = 3
  8. 票贩子C卖票,ticket = 1
  9. 票贩子B卖票,ticket = 4
  10. *****票卖光了*****
  11. *****票卖光了*****
  12. 票贩子A卖票,ticket = 2
  13. *****票卖光了*****

此时的程序看起来没有任何问题,但这是假象,当我们改动一下,比如在卖票操作中加入一些延迟操作来模拟网络延迟:


  
  1. class MyThread implements Runnable{
  2. private int ticket = 10; //总票数为十张
  3. @Override
  4. public void run() {
  5. while( true) {
  6. if( this.ticket > 0) {
  7. try {
  8. Thread.sleep( 100);
  9. } catch (InterruptedException e) {
  10. // TODO Auto-generated catch block
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket --);
  14. } else {
  15. System.out.println( "*****票卖光了*****");
  16. break;
  17. }
  18. }
  19. }
  20. }
  21. public class first {
  22. public static void main( String args[] ) throws Exception{
  23. MyThread mt = new MyThread();
  24. new Thread(mt, "票贩子A").start();
  25. new Thread(mt, "票贩子B").start();
  26. new Thread(mt, "票贩子C").start();
  27. }
  28. }

这时候有意思的就来了,我们多次执行程序后会发现,有时候会出现两个线程卖同一张票的操作,有时候甚至会出现卖光了还再卖的情况:


  
  1. 票贩子C卖票,ticket = 10
  2. 票贩子A卖票,ticket = 8
  3. 票贩子B卖票,ticket = 9
  4. 票贩子C卖票,ticket = 7
  5. 票贩子B卖票,ticket = 5
  6. 票贩子A卖票,ticket = 6
  7. 票贩子C卖票,ticket = 3
  8. 票贩子B卖票,ticket = 4
  9. 票贩子A卖票,ticket = 2
  10. 票贩子C卖票,ticket = 1
  11. *****票卖光了*****
  12. 票贩子B卖票,ticket = 0
  13. *****票卖光了*****
  14. 票贩子A卖票,ticket = -1
  15. *****票卖光了*****

这个时候因为追加了延迟问题就暴露出来了,但其实这个问题一直存在,那为什么会出现这种情况呢?我们来分析一下:

  • 在开始没有延迟的时候,三个线程在判断是否有票后就直接进行ticket--操作,没有问题;
  • 但是当加了延迟操作后,线程在判断后发现有票不会直接ticket--而是会先休眠,这时如果有别的线程再来判断,因为没有进行ticket--所以依然会判断有票,但依然会先休眠,这时如果前面的线程休眠未结束(网络延迟未结束)那后面的线程依然会进行上述判断;
  • 那么问题来了,如果这种情况出现在只剩下一张票的时候,第一个线程判断有票然后休眠、第二个线程因为第一个线程没有进行ticket--所以依然判断有票然后休眠、第三个线程因为第一个第二个线程没有进行ticket--所以依然判断有票然后休眠;
  • 当休眠结束后,三个线程分别进行三次ticket-- ,这时因为ticket = 1,所以会出现票数为0或-1时还再卖票的情况。

所以现在通过这样一个程序结构,所以我们的问题就是:在代码中有多个线程访问同一个资源的时候,如何保证数据的完整性,而这个操作就叫线程的异步(不同步),要想解决这个问题,就要使用线程的同步来解决。

线程同步

经过之前的分析我们就可以确定同步问题产生的主要原因了,下面就要进行同步问题的解决,结局同步问题的关键是“锁”,就是说在当某一个线程执行操作的时候,其他线程在外面等待。

如果想要在程序之中实现这把锁,就要使用synchronized关键字来实现,利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作中只允许一个线程执行。

  • 利用同步代码块进行处理

  
  1. synchronized(同步对象){
  2. 同步代码操作;
  3. }

一般要进行同步对象处理的时候可以采用当前对象this进行同步,我们来看一下:


  
  1. class MyThread implements Runnable{
  2. private int ticket = 10; //总票数为十张
  3. @Override
  4. public void run() {
  5. while( true) {
  6. synchronized( this) { //这里就表明每次只允许一个线程进行访问
  7. if( this.ticket > 0) {
  8. try {
  9. Thread.sleep( 100);
  10. } catch (InterruptedException e) {
  11. // TODO Auto-generated catch block
  12. e.printStackTrace();
  13. }
  14. System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket --);
  15. } else {
  16. System.out.println( "*****票卖光了*****");
  17. break;
  18. }
  19. }
  20. }
  21. }
  22. }

这样修改代码后就将网络延迟以及卖票的部分全部变为每次只允许一个线程操作,我们再来看一下结果:


  
  1. 票贩子A卖票,ticket = 10
  2. 票贩子A卖票,ticket = 9
  3. 票贩子A卖票,ticket = 8
  4. 票贩子A卖票,ticket = 7
  5. 票贩子A卖票,ticket = 6
  6. 票贩子A卖票,ticket = 5
  7. 票贩子C卖票,ticket = 4
  8. 票贩子C卖票,ticket = 3
  9. 票贩子C卖票,ticket = 2
  10. 票贩子C卖票,ticket = 1
  11. *****票卖光了*****
  12. *****票卖光了*****
  13. *****票卖光了*****

加入同步处理之后,整体的执行性能下降了,所以同步实际上会造成性能的降低。

  • 利用同步方法解决

只需要在方法定义上使用synchronized关键字即可,我们可以看一下:


  
  1. class MyThread implements Runnable{
  2. private int ticket = 10; //总票数为十张
  3. public synchronized boolean sale(){
  4. if( this.ticket > 0) {
  5. try {
  6. Thread.sleep( 100);
  7. } catch (InterruptedException e) {
  8. // TODO Auto-generated catch block
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket --);
  12. return true;
  13. } else {
  14. System.out.println( "*****票卖光了*****");
  15. return false;
  16. }
  17. }
  18. @Override
  19. public void run() {
  20. while( this.sale()) {
  21. ;
  22. }
  23. }
  24. }

这个时候也可以实现同步处理:


  
  1. 票贩子A卖票,ticket = 10
  2. 票贩子A卖票,ticket = 9
  3. 票贩子A卖票,ticket = 8
  4. 票贩子A卖票,ticket = 7
  5. 票贩子A卖票,ticket = 6
  6. 票贩子A卖票,ticket = 5
  7. 票贩子A卖票,ticket = 4
  8. 票贩子A卖票,ticket = 3
  9. 票贩子A卖票,ticket = 2
  10. 票贩子A卖票,ticket = 1
  11. *****票卖光了*****
  12. *****票卖光了*****
  13. *****票卖光了*****

这里我们提供两种同步途径:同步代码块与同步方法,在以后学习Java类库的时候会发现,系统上许多的类使用同步处理采用的都是同步方法,同时要注意同步会造成性能下降。

线程死锁

死锁是在进行多线程同步的处理中可能会产生的一种问题,所谓的死锁实际上是若干个线程彼此相互等待的概念。

下面通过一个简单的过桥例子来看一下:


  
  1. class A{
  2. public synchronized void say(B b) {
  3. System.out.println( "A:要想过桥,就要给钱!");
  4. b.get();
  5. }
  6. public synchronized void get() {
  7. System.out.println( "A:拿到钱了,过去吧!");
  8. }
  9. }
  10. class B{
  11. public synchronized void say(A a) {
  12. System.out.println( "B:让我过桥,过了就给!");
  13. a.get();
  14. }
  15. public synchronized void get() {
  16. System.out.println( "B:我过来了,把钱给你!");
  17. }
  18. }
  19. public class first implements Runnable{
  20. private A a = new A();
  21. private B b = new B();
  22. @Override
  23. public void run() {
  24. a.say(b);
  25. }
  26. public first() {
  27. new Thread( this).start();
  28. b.say(a);
  29. }
  30. public static void main( String args[] ) {
  31. new first();
  32. }
  33. }

这时候执行程序就会发现卡住了,线程在相互消耗不继续向下执行了,而右上角显示程序还未执行结束:

这时候可以看到造成死锁的原因是两个线程彼此在互相等待着,等待着对方先让出资源。我们将其中一个方法的synchronized关键字去掉就能看到线程可以继续执行:


  
  1. public void say(A a) {
  2. System. out.println( "B:让我过桥,过了就给!");
  3. a. get();
  4. }

随便一个synchronized去掉就可以,但是这个程序不需要做什么重要说明。因为死锁实际上是开发中出现的一种不确定的状态,有的时候代码如果处理不当会不定期出现死锁,这是开发中正常的调试问题,我们这个程序是强制锁上的所以没有什么研究意义。

若干个线程访问同一资源时一定要进行同步处理,而过多的同步会造成死锁。


                                              “生产者-消费者”模型


我们现在已经清楚了多线程的基本组成的操作与多线程在进行同步访问的时候所产生的问题,现在我们就看一个多线程操作的经典案例:“生产者-消费者”模型。

在多线程的开发过程中,最为著名的就是 “生产者-消费者”模型操作,该操作的主要流程如下:

  • 生产者负责信息内容的生产;
  • 每当生产者生产完成一项完整的信息之后,消费者要从中取走信息;
  • 如果生产者没有生产完成,消费者要等待生产者完成消费;
  • 如果消费者还没有对信息进行消费,那么生产者就应该等待消费者消费后再生产。

                                                 “生产者-消费者”模型基本实现

可以将生产者与消费者定义为两个独立的线程对象,但是对于生产的数据,可以使用如下的组成:

  • 数据1:title = 紫薇一号、content = 紫薇星公务员;
  • 数据1:title = 紫薇二号、content = 紫薇星司机;

既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就要有一个数据的保存集中点,那么我们就可以单独定义一个Message类来实现数据的保存:

接下来我们就来实现一下程序基本结构:


  
  1. class Producer implements Runnable{
  2. private Message msg;
  3. public Producer(Message msg) {
  4. this.msg = msg;
  5. }
  6. @Override
  7. public void run() {
  8. for( int x = 0; x < 100; x ++) {
  9. if(x % 2 == 0) {
  10. this.msg.setTitle( "紫薇一号");
  11. try {
  12. Thread.sleep( 100);
  13. } catch (InterruptedException e) {
  14. // TODO Auto-generated catch block
  15. e.printStackTrace();
  16. }
  17. this.msg.setContent( "紫薇星公务员");
  18. } else {
  19. this.msg.setTitle( "紫薇二号");
  20. try {
  21. Thread.sleep( 100);
  22. } catch (InterruptedException e) {
  23. // TODO Auto-generated catch block
  24. e.printStackTrace();
  25. }
  26. this.msg.setContent( "紫薇星司机");
  27. }
  28. }
  29. }
  30. }
  31. class Consumer implements Runnable{
  32. private Message msg;
  33. public Consumer (Message msg) {
  34. this.msg = msg;
  35. }
  36. @Override
  37. public void run() {
  38. for( int x = 0; x < 100; x ++) {
  39. try {
  40. Thread.sleep( 10);
  41. } catch (InterruptedException e) {
  42. // TODO Auto-generated catch block
  43. e.printStackTrace();
  44. }
  45. System.out.println( this.msg.getTitle() + " - " + this.msg.getContent());
  46. }
  47. }
  48. }
  49. class Message {
  50. private String title;
  51. private String content;
  52. public String getTitle() {
  53. return this.title;
  54. }
  55. public void setTitle(String title) {
  56. this.title = title;
  57. }
  58. public String getContent() {
  59. return this.content;
  60. }
  61. public void setContent(String content) {
  62. this.content = content;
  63. }
  64. }
  65. public class first {
  66. public static void main( String args[] ) throws Exception{
  67. Message msg = new Message();
  68. new Thread( new Producer(msg)).start(); //启动生产者线程
  69. new Thread( new Consumer(msg)).start(); //启动消费者线程
  70. }
  71. }

这样就可以了,为了方便观察问题在生产处和消费处都加入了休眠操作,这时候结果如下:

现在我们可以很明显的看到问题,公务员的一号变成了司机,而二号是司机却变成了公务员,甚至在刚开始生产时还有null的出现。通过这整个代码的执行可以发现两个问题:

  • 数据不同步了;
  • 应该是生产一个取走一个,但出现了重复生产和重复取出的情况。

这些问题就是我们要解决的核心。

                                                 解决“生产者-消费者”模型的问题

如果要解决问题,首先要解决的就是数据同步处理的问题,而要解决数据处理的问题最好就是使用synchronized关键字定义同步代码块或者同步方法,这个时候对于同步的处理就可以在Messgae类中直接完成。

我们来解决一下:


  
  1. class Producer implements Runnable{
  2. private Message msg;
  3. public Producer(Message msg) {
  4. this.msg = msg;
  5. }
  6. @Override
  7. public void run() {
  8. for( int x = 0; x < 100; x ++) {
  9. if(x % 2 == 0) {
  10. this.msg.set( "紫薇一号", "紫薇星公务员");
  11. } else {
  12. this.msg.set( "紫薇二号", "紫薇星司机");
  13. }
  14. }
  15. }
  16. }
  17. class Consumer implements Runnable{
  18. private Message msg;
  19. public Consumer (Message msg) {
  20. this.msg = msg;
  21. }
  22. @Override
  23. public void run() {
  24. for( int x = 0; x < 100; x ++) {
  25. System.out.println( this.msg.get());
  26. }
  27. }
  28. }
  29. class Message {
  30. private String title;
  31. private String content;
  32. public synchronized void set(String title, String content) {
  33. this.title = title;
  34. try {
  35. Thread.sleep( 100);
  36. } catch (InterruptedException e) {
  37. // TODO Auto-generated catch block
  38. e.printStackTrace();
  39. }
  40. this.content = content;
  41. }
  42. public synchronized String get() {
  43. try {
  44. Thread.sleep( 10);
  45. } catch (InterruptedException e) {
  46. // TODO Auto-generated catch block
  47. e.printStackTrace();
  48. }
  49. return this.title + " - " + this.content;
  50. }
  51. }
  52. public class first {
  53. public static void main( String args[] ) throws Exception{
  54. Message msg = new Message();
  55. new Thread( new Producer(msg)).start(); //启动生产者线程
  56. new Thread( new Consumer(msg)).start(); //启动消费者线程
  57. }
  58. }

在进行同步处理的时候肯定要有一个同步处理对象,此时肯定要将同步操作交给Mesage类处理是最合适的,我们来看一下这次的结果,这次因为产生同步操作会慢一下,仍然只截取一部分能说明问题的:

这个时候我们发现数据一致性已经保持了,但是重复生产重复消费的问题还没有解决,所以同步只能解决数据一致性处理。

                                                          线程的等待与唤醒

而为了解决现在的重复问题,最好的方式就是使用线程的等待与唤醒机制:可以在Message上增加一个指示灯机制,当Message中没有产品时,出现绿灯,此时生产者可以生产但消费者不能消费;当生产者生产一个产品进入Message后指示灯变红,此时生产者不能生产但消费者不可以消费;当消费者消费后Message中又没有产品时,出现绿灯,不断循环。

这就是等待与唤醒机制,但是这个机制主要是依靠Object类中提供的方法处理的:

  • 死等机制:public final void wait() throws InterruptedException;
  • 设置时间的等待机制:public final native void wait(long timeout) throws InterruptedException;
  • 设置时间的等待机制:public final void wait(long timeout, int nanos) throws InterruptedException;
  • 唤醒第一个等待线程:public final native void notify();
  • 唤醒全部等待线程:public final native void notifyAll();
  • 如果现在有若干个等待线程,notify()是用来唤醒第一个等待线程的,而其他的线程继续等待;
  • 而notifyAll是用来唤醒所有的等待线程,哪个线程的优先级高一些就有可能先执行。

对于当前的问题应该通过Message类来处理,所以应该修改Message类:


  
  1. class Producer implements Runnable{
  2. private Message msg;
  3. public Producer(Message msg) {
  4. this.msg = msg;
  5. }
  6. @Override
  7. public void run() {
  8. for( int x = 0; x < 100; x ++) {
  9. if(x % 2 == 0) {
  10. this.msg.set( "紫薇一号", "紫薇星公务员");
  11. } else {
  12. this.msg.set( "紫薇二号", "紫薇星司机");
  13. }
  14. }
  15. }
  16. }
  17. class Consumer implements Runnable{
  18. private Message msg;
  19. public Consumer (Message msg) {
  20. this.msg = msg;
  21. }
  22. @Override
  23. public void run() {
  24. for( int x = 0; x < 100; x ++) {
  25. System.out.println( this.msg.get());
  26. }
  27. }
  28. }
  29. class Message {
  30. private String title;
  31. private String content;
  32. private boolean flag = true; //表示生产或消费的时机,指示灯作用
  33. //flag == true表示允许生产但不允许消费
  34. //flag == false表示允许消费但不允许生产
  35. public synchronized void set(String title, String content) {
  36. if(! this.flag) { //表示无法生产,应该等待被消费
  37. try {
  38. super.wait();
  39. } catch (InterruptedException e1) {
  40. // TODO Auto-generated catch block
  41. e1.printStackTrace();
  42. }
  43. }
  44. this.title = title;
  45. try {
  46. Thread.sleep( 100);
  47. } catch (InterruptedException e) {
  48. // TODO Auto-generated catch block
  49. e.printStackTrace();
  50. }
  51. this.content = content;
  52. this.flag = false; //已经生产过了
  53. super.notify(); //唤醒等待的线程
  54. }
  55. public synchronized String get() {
  56. if( this.flag) {
  57. try {
  58. super.wait();
  59. } catch (InterruptedException e1) {
  60. // TODO Auto-generated catch block
  61. e1.printStackTrace();
  62. }
  63. }
  64. try {
  65. Thread.sleep( 10);
  66. } catch (InterruptedException e) {
  67. // TODO Auto-generated catch block
  68. e.printStackTrace();
  69. }
  70. try {
  71. return this.title + " - " + this.content;
  72. } finally { //不管如何都要执行
  73. this.flag = true;
  74. super.notify();
  75. }
  76. }
  77. }
  78. public class first {
  79. public static void main( String args[] ) throws Exception{
  80. Message msg = new Message();
  81. new Thread( new Producer(msg)).start(); //启动生产者线程
  82. new Thread( new Consumer(msg)).start(); //启动消费者线程
  83. }
  84. }

这时候就是可以正常执行生产一个消费一个的程序,我们来看一下:

这种处理形式就是我们多线程的最原始的处理方法,整个的等待、同步、唤醒机制都有开发者自行通过原生代码进行控制。


                                                  多线程深入话题


                                                             优雅的停止线程

在多线程的操作中如果要启动多线程肯定要使用Thread类中的start()方法,而如果要对多线程进行停止处理,在Thread类中原本有一个stop()方法,但是我们从源代码中找到这个方法,会发现:

  • 停止多线程

当我们看到图中的第967行就一目了然了,这个方法不仅已经过期,而且要被移走了,从JDK1.2开始就已经被废除了,而且也不建议出现在代码中,而除了stop()之外有很多也被禁用了:

  • 销毁多线程

  • 挂起线程(暂停执行)

  • 恢复挂起的线程

之所以废除这些方法主要是因为这些方法有可能导致线程的死锁,所以从JDK1.2开始就都不建议使用了。这个时候要想实现线程的停止就要通过一种柔和的方式来进行:


  
  1. public class first {
  2. public static boolean flag = true;
  3. public static void main( String args[] ) throws Exception{
  4. new Thread(()->{
  5. long num = 0;
  6. while(flag) {
  7. try {
  8. Thread.sleep( 50);
  9. } catch (InterruptedException e) {
  10. // TODO Auto-generated catch block
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread().getName() + "正在运行、num = " + num ++);
  14. }
  15. }, "执行线程").start();
  16. Thread.sleep( 200); //先运行200毫秒
  17. flag = false; //停止
  18. }
  19. }

这个时候我们可以尝试一下看能不能在运行四次后停止。我们设置flag的内容改变使线程停止的方式其实不是说停就停的,因为如果现在有其他的线程去控制这个flag,那么这个时候就要先判断flag中的内容才能做出操作,如果有其它操作的话可能在200毫秒时还没判断就又执行了一次,所以这个停止不是即时的。

但因为强制立刻停止有可能会造成死锁,所以这种方式是最方便的也是最柔和的方式。


                                                                 守护线程

守护的意思其实就是保护,现在假设有一个人有一个保镖,那么这个保镖一定是在这个人活着的情况下才有用的,人没了就什么都没了。所以在多线程中可以进行守护线程的定义,如果主线程的程序或其他线程的程序还在执行,那么守护线程将一直存在并且运行在后台状态。

在Thread类中提供有如下的守护线程的操作方法:

  • 设置守护线程:public final void setDaemon(boolean on) ;
  • 判断是否为守护线程:public final boolean isDaemon();

我们来操作一下:


  
  1. public class first {
  2. public static boolean flag = true;
  3. public static void main( String args[] ) throws Exception{
  4. Thread userThread = new Thread(()->{
  5. for( int x = 0; x < Integer.MAX_VALUE; x ++){
  6. try {
  7. Thread.sleep( 100);
  8. } catch (InterruptedException e) {
  9. // TODO Auto-generated catch block
  10. e.printStackTrace();
  11. }
  12. System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
  13. }
  14. }, "用户线程");
  15. Thread daemonThread = new Thread(()->{
  16. for( int x = 0; x < Integer.MAX_VALUE; x ++){
  17. try {
  18. Thread.sleep( 100);
  19. } catch (InterruptedException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
  24. }
  25. }, "守护线程");
  26. userThread.start();
  27. daemonThread.start();
  28. }
  29. }

这是一个简单的线程执行的程序,这样就说明有一个用户线程和一个没有被定义的守护线程,当程序执行时用户线程和未定义的守护线程会交替执行,而当我们将用户线程代码块中的MAX_VALUE改为10之后,就会看到当用户线程执行十次之后,守护线程还是在不断执行。

现在我们将守护线程设置一下:


  
  1. public class first {
  2. public static boolean flag = true;
  3. public static void main( String args[] ) throws Exception{
  4. Thread userThread = new Thread(()->{
  5. for( int x = 0; x < 10; x ++){
  6. try {
  7. Thread.sleep( 100);
  8. } catch (InterruptedException e) {
  9. // TODO Auto-generated catch block
  10. e.printStackTrace();
  11. }
  12. System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
  13. }
  14. }, "用户线程");
  15. Thread daemonThread = new Thread(()->{
  16. for( int x = 0; x < Integer.MAX_VALUE; x ++){
  17. try {
  18. Thread.sleep( 100);
  19. } catch (InterruptedException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
  24. }
  25. }, "守护线程");
  26. daemonThread.setDaemon( true); //设置守护线程
  27. userThread.start();
  28. daemonThread.start();
  29. }
  30. }

这时候再看运行结果就变成了守护线程在守护着用户线程,当用户线程执行完毕后守护线程也随即停止:


  
  1. 守护线程正在运行、x = 0
  2. 用户线程正在运行、x = 0
  3. 守护线程正在运行、x = 1
  4. 用户线程正在运行、x = 1
  5. 守护线程正在运行、x = 2
  6. 用户线程正在运行、x = 2
  7. 用户线程正在运行、x = 3
  8. 守护线程正在运行、x = 3
  9. 用户线程正在运行、x = 4
  10. 守护线程正在运行、x = 4
  11. 守护线程正在运行、x = 5
  12. 用户线程正在运行、x = 5
  13. 用户线程正在运行、x = 6
  14. 守护线程正在运行、x = 6
  15. 用户线程正在运行、x = 7
  16. 守护线程正在运行、x = 7
  17. 守护线程正在运行、x = 8
  18. 用户线程正在运行、x = 8
  19. 守护线程正在运行、x = 9
  20. 用户线程正在运行、x = 9

所有的守护线程都是围绕在用户线程的周围,如果程序执行完毕了,守护线程也没有存在的必要了。而在JVM中最大的守护线程就是GC线程,程序执行中使用GC线程进行垃圾回收,程序执行中GC线程会一直存在,如果程序执行完毕,那么GC线程也会消失。


                                                             volatile关键字

在多线程的定义之中volatile关键字主要是在属性定义上使用的,表示此属性直接进行数据操作而不进行副本的拷贝处理,所以在一些书上会错误的将其理解为同步属性。

比如我们来到刚才讲同步时的卖票程序例子:


  
  1. class MyThread implements Runnable{
  2. private int ticket = 10; //总票数为十张
  3. @Override
  4. public void run() {
  5. while( true) {
  6. if( this.ticket > 0) {
  7. System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket --);
  8. } else {
  9. System.out.println( "*****票卖光了*****");
  10. break;
  11. }
  12. }
  13. }
  14. }
  15. public class first {
  16. public static void main( String args[] ) throws Exception{
  17. MyThread mt = new MyThread();
  18. new Thread(mt, "票贩子A").start();
  19. new Thread(mt, "票贩子B").start();
  20. new Thread(mt, "票贩子C").start();
  21. }
  22. }

这个例子大家知道有问题,但是加了volatile之后有时会出现无错情况:private volatile int ticket = 10; //总票数为十张。但是仍然会出现不时的重复,因为加了之后看上去好像没有错了,所以有时会被人说是同步属性,但这个属性不是描述同步的,它描述的是直接操作。

在正常进行的变量处理的时候往往会经历下面几个步骤:

  • 获取变量原有的数据内容;
  • 利用副本为变量进行数学计算;
  • 将计算后的变量,保存到原始空间之中;

而一个属性上追加了volatile关键字之后就表示不使用副本而直接操作原始变量,相当于节约了拷贝副本、重新保存的步骤。对于原始的多线程的操作,永远都是存在这几个步骤的。所以要想带来性能上的提升,不去操作副本直接操作原始变量是最合适的。如果加了volatile关键字是在告诉我们,在进行数据操作的时候,这些数据不是通过拷贝副本来进行操作的,而是通过直接对原始变量来操作的,这个时候在程序中要添加同步操作的照样要添加。

  • volatile与synchronized的区别
volatile synchronized
主要在属性上使用 在代码块与方法上使用

无法描述同步的处理,是一种直接对内存的处理,避免了副本操作

作用是实现同步

线程与多线程的知识点就在这里结束了,我们下次见👋


转载:https://blog.csdn.net/qq_39732867/article/details/104607574
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场