Java并行程序基础

  • Java并行程序基础
  • 有关线程你必须知道的事
  • 初始线程:线程的基本操作
  • 新建线程
  • 终止线程
  • 线程中断
  • 等待(wait)和通知(notify)
  • 挂起(suspend)和继续执行(resume)线程
  • 等待线程结束(join)和谦让(yield)
  • volatile与Java内存模型(JMM)
  • 分门别类的管理:线程组
  • 驻守后台:守护线程(Daemon)
  • 先干重要的事:程序优先级
  • 线程安全的概念与synchronized
  • 程序中的幽灵:隐蔽的错误
  • 无提示的错误案例
  • 并发下的ArrayList
  • 并发下诡异的HashMap
  • 初学者常见问题:错误的加速


Java并行程序基础

有关线程你必须知道的事

进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
程序:程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程:线程是轻量级进程,是程序执行的最小单位。
进程是线程的容器。
使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。

线程生命周期的状态图:

java 高并发事务怎么设计一个转账系统 java 高并发程序设计_Java


NEW状态表示刚刚创建的线程,这种线程还没开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。

注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED的线程也不能再回到RUNNABLE状态。

初始线程:线程的基本操作

新建线程

新建线程很简单,只需new一个线程对象,并将它start()起来即可。start()后,就会新建一个线程并让这个线程执行run()方法。
注意:不要用run()来开启新线程。它只会再当前线程中,串行执行run()中的代码。

Thread类中有一个非常重要的构造方法:

public Thread(Runnable target)

注意:默认的Thread.run()就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,更为合理。

public class CreateThread implements Runnable{
	public static void main(String[] args){
		Thread t1 = new Thread(new CreateThread());
		t1.start();
	}

	@Override
	public void run(){
		System.out.printIn("Oh, I am Runnable");
	}
}

上述代码实现了Runnable接口,并将该实例传入Thread。这样避免重载Thread.run(),单纯使用接口来定义Thread,也是最常用的做法。

终止线程

一般来说,线程在执行完毕后就会结束,无须手动关闭。但是,一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。

那如何正常的关闭一个线程呢?查阅JDK,你不难发现Thread提供了一个stop()方法。stop()可以立即将一个线程终止,非常方便,但你会发现stop()方法是一个被标注为废弃的方法。为什么stop()被废弃而不推荐使用呢?原因是stop()方法太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

线程中断

在Java中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。

严格地讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到stop()方法的老问题。

与线程中断有关的,有三个方法,这三个方法看起来很像,要注意不要混淆和误用:

public void Thread.interrupt()					//中断线程
public boolean Thread.isInterrupted()			//判断是否被中断
public static boolean Thread.interrupted()		//判断是否被中断,并清除当前中断状态

了解下Thread.sleeo()函数,它的签名如下:

public static native void sleep(long millis) throws InterruptedException

Thread.sleep()方法会让当前线程休眠若干时间,他会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。

注意:Tread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

等待(wait)和通知(notify)

为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这两个方法。
这两个方法的签名如下:

public final void wait() throws  InterruptedException
public final native void notify()

java 高并发事务怎么设计一个转账系统 java 高并发程序设计_System_02


java 高并发事务怎么设计一个转账系统 java 高并发程序设计_Java_03


注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

挂起(suspend)和继续执行(resume)线程

通过阅读JDK有关Thread类的API文档,会发现线程挂起(suspend)和继续执行(resume)这两个接口,这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()操作后,才能继续指定。但是它们早已被标注为废弃方法,并不推荐使用。

不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看。居然还是Runnable,这也会严重影响我们对系统当前状态的判断。

java 高并发事务怎么设计一个转账系统 java 高并发程序设计_java_04


下图为演示suspend()问题的代码:

package com.fwx.test;


/**
 * @author fwx
 * @date 2022/12/17
 */
public class BadSuspend {
    public static Object u = new Object();
    static ChangObjectThread t1 = new ChangObjectThread("t1");
    static ChangObjectThread t2 = new ChangObjectThread("t2");
    public static class ChangObjectThread extends Thread{
        public ChangObjectThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            synchronized (u){
                System.out.println("in "+getName());
                Thread.currentThread().suspend();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

获得的输出:

in t1
in t2

这表明线程先后进入临界区,但是程序不会退出。而是挂起。使用jstack命令打印系统的线程信息可以看到:

"t2@483" prio=5 tid=0xf nid=NA runnable
  java.lang.Thread.State: RUNNABLE
	  at java.lang.Thread.suspend0(Thread.java:-1)
	  at java.lang.Thread.suspend(Thread.java:1037)
	  at com.fwx.test.BadSuspend$ChangObjectThread.run(BadSuspend.java:20)
	  - 锁定 <0x1e8> (java.lang.Object)

下面利用wait()和notify()方法,在应用层面实现suspend()和resume()功能:

package com.fwx.test.suspendandresume;


/**
 * @author fwx
 * @date 2022/12/17
 */
public class GoodSuspend {

 public static Object u = new Object();

 public static class ChangeObjectThread extends Thread{
  volatile boolean suspendMe = false;

  public void suspendMe(){
   suspendMe = true;
  }

  public void resumeMe(){
   suspendMe = false;

   synchronized (this){
    notify();
   }
  }

  @Override
  public void run() {
   while (true){
    synchronized (this){
     while (suspendMe){
      try {
       wait();
      } catch (InterruptedException e) {
       throw new RuntimeException(e);
      }
     }
    }

    synchronized (u){
     System.out.println("in changeObjectThread");
    }
    Thread.yield();
   }
  }
 }
 public static class ReadObjectThread extends Thread{
  @Override
  public void run() {
   while (true){
    synchronized (u){
     System.out.println("in ReadObjectThread");
    }
    Thread.yield();
   }
  }
 }

 public static void main(String[] args) throws InterruptedException {
  ChangeObjectThread t1 = new ChangeObjectThread();
  ReadObjectThread t2 = new ReadObjectThread();
  t1.start();
  t2.start();
  Thread.sleep(5000);
  t1.suspendMe();
  System.out.println("suspend t1 2 sec");

  Thread.sleep(2000);
  System.out.println("resume t1");
  t1.resumeMe();
 }

}

截取部分输出:

java 高并发事务怎么设计一个转账系统 java 高并发程序设计_System_05

等待线程结束(join)和谦让(yield)

很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能,如下所示,显示了两个join()方法:

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

第一个join()方法表示无限等待,它会一直阻塞当前线程,直至目标线程执行完毕。
第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
join()的本质是让调用线程wait()在当前线程对象实例上。下面是JDK中join()实现的核心代码片段:

while (isAlive()) {
                wait(0);
            }

可以看到,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行。因此,值得注意的一点是:不要在应用程序中,在Thread对象实例上使用类似wait()或者notify()等方法,因为这很有可能会影响系统API的工作,或者被系统API所影响。

另一个比较有趣的方法,是Thread.yield(),它的定义如下:

public static native void yield();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不代表当前线程不执行了。
如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。

volatile与Java内存模型(JMM)

volatile:易变的,不稳定的。
volatile对于保证操作的原子性是有非常大的帮助的。但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的原子性。
此外,volatile也能保证数据的可见性和有序性。如下例子:

package com.fwx.test.volatileTest;

/**
 * @author fwx
 * @date 2022/12/17
 */
public class NoVisibility {

    private static boolean ready;

    private static int num;

    private static class ReaderThread extends Thread{
        public void run(){
            while (!ready);
            System.out.println(num);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        num = 42;
        ready = true;
        Thread.sleep(10000);
    }


}

num和ready都没被volatile修饰,程序进入死循环,不会输出num。

分门别类的管理:线程组

在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
注意:线程组也有stop()方法,它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()相同的问题,因此使用时也需要格外谨慎。

驻守后台:守护线程(Daemon)

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作。如果用户线程全部结束,这也意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出。

先干重要的事:程序优先级

Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这只是一个概率问题。
在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:

/**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

线程安全的概念与synchronized

并行程序开发的一大关注重点就是线程安全。一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序本身也就没任何意义了。

java 高并发事务怎么设计一个转账系统 java 高并发程序设计_开发语言_06


关键字synchronized可以有多种用法。这里做一个简单的整理。

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类得锁。
    除了用于线程同步、确保线程安全外,synchronized还可以保证线程间得可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被synchronized限制的多个线程是串行执行的)。

程序中的幽灵:隐蔽的错误

无提示的错误案例

int v1 = 1073741827;
int v2 = 1431655768;
System.out.println("v1="+v1);
System.out.println("v2="+v2)
int ave = (v1+v2)/2;
System.out.println("ave="+ave);

结果:

v1=1073741827
v2=1431655768
ave=-894784850

显然,v1+v2的结果导致了int的溢出。

并发下的ArrayList

ArrayList是一个线程不安全的容器。如果再多线程中使用ArrayList,可能会导致程序出错。
例:t1和t2两个线程同时向一个ArrayList容器中添加容器。他们各添加1000000个元素,因此我们期望最后可以有2000000个元素在ArrayList中。但如果你用ArrayList去存储可能会得到三种结果。

  • 第一,程序正常结束,ArrayList的最终大小确实是2000000。这说明即使并行程序有问题,也未必会每次都表现出来。
  • 第二,程序抛出异常:
Exception in thread "Thread-0" java.lang ArrayIndexOutOfBoundsException: 22
	at java.util.ArrayList.add(ArrayList.java:441)
	at geym.conc.ch2.notsafe.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:12)
	at java.lang.Thread.run(Thread.java:724)
	1000015
  • 第三,出现了一个非常隐蔽的错误,比如打印如下值作为ArrayList的大小:
1793758

显然,这是由于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一个位置进行赋值导致的。如果出现这种问题,那么很不幸,你就得到了一个没用错误提示的错误。并且,他们未必是可以复现的。
注意:改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

并发下诡异的HashMap

HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也会遇到意想不到的错误。(详细看2.8.3)
多线程环境下推荐使用ConcurrentHashMap代替HashMap。

初学者常见问题:错误的加速

(详细看2.8.4)