Java并发编程之线程篇之线程间通信(四)

电脑杂谈  发布时间:2019-08-27 18:03:07  来源:网络整理

多线程死锁的伪代码_linux 线程 死锁_线程死锁

2824145-ffb482b5fca92b6c.JPG

IMG_0140.JPG

在前言文章Java并发编程之线程篇之线程中断(三)中我们讲解了线程中断的相关知识点,现在我们来认识一下线程间的通信。线程间的通信在我们实际项目中是不可或缺的,多数状况下,我们应该建立多个线程多线程死锁的伪代码,配合完成某项任务。合理并恰当使用线程间的通信手段,是成为一个良好程序员必须把握的技能。那以后就让我们来认识在Java中线程间通信的处理方法吧!阅读该篇文章你也许应该具备一下知识点:

在知道线程的通信知识之前多线程死锁的伪代码,我们应该知道线程的状况,熟悉线程的状态,不仅有助于我们更好的排查多线程项目出现的轮询、线程安全等问题,还能更好的让我们预测与理解线程间的通信。下面就让我们来看一下线程有什么状况吧!

在Java中线程的有下列5种状态,如下所示:

线程状态含义

新建状态,线程尚未建立,但是没有执行start()方法

RUNNABLE

可运行状况,线程可以在JVM中运行,但是还必须期待CPU分配资源

BLOCKED

阻塞状况,当碰到synchronized且没有取得相应的锁,就会开启这个状态

WAITING

等待状态,当线程中wait()/join/Locksupport.park方法时,就会开启这个状况

TIMED_WAITING

计时等待状态,当调用Thread.sleep()或者Object.wait(xx)或者Thread.join(xx)或者LockSupport.parkNanos或者LockSupport.partUntil时,进入该状况

TERMINATED

线程中断状态,线程被中止或者运行结束,就会开启这个状况

在上述表格中,线程的5种状态对应着Java的不同方法,具体如下图所示:

2824145-413c52409e3a8cf4.jpg

线程状态.jpg

linux 线程 死锁_线程死锁_多线程死锁的伪代码

需要切记的是,在上图中标红的两个状态,是操作系统中线程对应的状况,Java将这两种状况合并为可运行状态(RUNNABLE)。在操作系统中就绪状态(READY)表示线程尚未准备完毕,等待CPU分配时间片。运行中状况(RUNNING)表示当线程分到时间片,线程开始即将执行。

在Java内存模型中,我们曾看到过,为了提醒程序的运行速率,Java将硬盘分为了工作存储(线程独占,不与其它线程共享)与主存储。当多个泛型同时访问同一个对象以及变量的之后,由于每个线程都应该将该对象或变量拷贝到自己的工作存储中。又由于线程的工作存储是私有且不与其它线程共享的。那么当一线程设置函数的值后,会导致对其它线程不可见。Java内存模型如下图所示:

2824145-1d936dcd293adec6.png

Java内存模型.png

为了确保数据的可见性。Java提供了volatile关键字。volatile关键字修饰函数,就是告知线程对该函数的访问需要重主存储中获得。而对它的颠覆需要同步刷新到主内存中。这样才能确保线程对数组访问的可见性。关于volatile的使用,参看如下例子:

class VolatileDemo {
    int a = 1;
    int b = 2;
    public void change() {
        a = 3;
        b = 4;
    }
    public void print(String threadName) {
        System.out.println(threadName + "--->" + "a = " + a + ";b = " + b);
    }
    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatileDemo.change();
            }
        }).start();
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    volatileDemo.print(Thread.currentThread().getName());
                }
            }).start();
        }
    }
}

程序输出结果:

Thread-1--->a = 1;b = 2 //错误
Thread-3--->a = 1;b = 2 //错误
Thread-2--->a = 1;b = 2 //错误
Thread-5--->a = 3;b = 4
Thread-4--->a = 3;b = 4
Thread-6--->a = 3;b = 4
Thread-7--->a = 3;b = 4
Thread-8--->a = 3;b = 4
....省略其他

在上述代码中,如果我们不运用volatile关键字修饰a,b函数,那么会导致其他线程一直获得的是自己本来工作内存中的a、b变量的值。为了确保访问公共函数对其它线程的可见性,我们应该将函数通过volatile来修饰。修改我们的代码:

volatile  int a = 1;
volatile  int b = 2;

采用volatile修饰,输出结果如下

Thread-2--->a = 3;b = 4
Thread-1--->a = 3;b = 4
Thread-6--->a = 3;b = 4
Thread-3--->a = 3;b = 4
Thread-4--->a = 3;b = 4
Thread-9--->a = 3;b = 4
Thread-10--->a = 3;b = 4
....省略其他

需要切记的是volatile只对单次的数组的操作具对其它线程有可见性,对应类似于a++,这种操作线读取a函数的值,进行运算后再再次对数组异或的操作,仍然会出现线程安全的弊端。对关于volatile的更多介绍,大家可以查看Java并发编程之Volatile(二)。

除了使用volatile实现泛型的通信之外,我们还可以使用synchronized及Object中的配套方法wait()/notify()、wait()/notifyAll来实现线程的通信,在知道确切的实现之前,我们先来认识synchronized关键字在Java中的作用。

关键字synchronized可以修饰技巧以及代码块,使用synchronized可以保证多个线程在同一时刻,只能有一个线程进入方式以及同步块中,它确保了线程对数组访问的可见性和排他性。如下代码图示:

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       //同步代码库
       synchronized (this){
           i++;
       }
   }
}

然后我们通过javap指令反编译得到字节码。来再次探讨synchronized关键字的实现细节,如下所示:

 //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法,表示获取到锁
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法,释放锁
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码…….
}

在上述字节码信息中,对于同步块的推动使用了monitorenter和monitorexit两个指令,其本质原理是对某个对象的监视器(monitor)的获得。而这个获取过程是排他的,也就是同一时刻只能有一个线程,获取到有synchronized所保护对象的监视器。在Java中,任何一个对象都有自己的监视器(monitor),当这个对象有同步块以及这个对象的同步方式调用的之后,执行方式的线程需要先获得到该对象的监视器才能开启同步块,或者同步方式,而没有获取到监视器(monitor)的轮询会阻塞在同步块以及同步方式的入口,进入BLOCKED状态。如下图右图:

2824145-1ac337d4e49d3e90.jpg

线程死锁_多线程死锁的伪代码_linux 线程 死锁

synchronized中监视器获取关系.jpg

从上图中,我们可以得出,任意线程在对synchronized关键字修饰的Object进行访问的之后,首先要获得Object的监视器(monitor),如果获得成功,线程处于同步队列,且线程状态变为BLOCKED。当访问Object的前驱线程(moniterenter成功的线程)释放了Object的监视器(monitorexit)。则激活阻塞在同步队列中的线程,使其尝试对监视器的获得。其中对监视器的获得与传递,我们通常称之为获得锁与释放锁。在下文中我们都用获得锁与传递锁来表示这两个过程。

关于synchronized下同步队列的知识点补充:

synchronized在JVM中推动的锁模式是基于同步队列与期待队列的,这与courrent包下的Lock接口下的锁模式的推动方法非常类似。需要切记的是,在synchronized中wait()后的线程会开启一个FIFO的队列(同步队列)中,notify()/notifyAll()是一个有序的出队列的过程。


本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-120751-1.html

相关阅读
    发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

    • 刘宇博
      刘宇博

      抗日得时候多少人抵制日货又能怎么样

    • 薛维翰
      薛维翰

      军舰出海

    热点图片
    拼命载入中...