Java并发编程基础

Java并发程序的设计

并发的三大特性:原子性,可见性和有序性。

原子性

原子性指的是一个操作一旦开始就不可中断。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

显然对于串行程序来说,可见性问题是不存在的。因为在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。

但是在并行程序中修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。可以在其他线程中修改了一个变量,但是因为在其他CPU的缓存或者寄存器中,没有被即时更新到主存中,而另一个CPU从主存中读取的值可以是之前的值(失效的数据)。

Java中使用volatile来保证可见性。

重排序

  1. 重排序

在程序执行的过程中可能会对指令进行重排序。

指令不排序的规则:

  • 程序顺序原则:一个线程内保证语义的串行性。

  • valatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性。

  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。

  • 传递性:A先于B,B先于C,那么A必然先于C

  • 线程的start()方法先于它的每一个动作。

  • 线程的所有操作先于线程的终结(Thread.join())

  • 线程的中断(interrupt)先于被中断线程的代码。

  • 对象的构造函数执行、结束先于finalize()方法。

进程和线程的区别

进程(Process) 是计算机中的程序关于某一个数据集合上的一次运行活动,是系统进行资源和调度的基本单位,是操作系统的基础。在早起面向进程的计算机结构中,进程是线程的容器。程序指令和数据机器组织的描述,进程是程序的实体。

线程是轻量级的进程,是程序的最小执行单位。使用多线程而不是多进程进行并发程序的设计,是因为多线程间的切换和调度的成本远远小于进程。

所有线程的状态都在Thread中的枚举类中定义,如下所示:


public enum State {

    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;

}

NEW状态表示刚刚创建线程,这种线程还没开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程开始执行的时候,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了,如果线程在执行的时候遇见了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求锁。WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有时间的等待。

一般来说,WAITING的线程是正在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。

等待(wait)和通知(notify)

JDK提供了wait和notifty是为了增加线程之间的协作,这两个方法并不是在Thrad类中的,而是在Object类中的。

签名如下:


public final wait() throw InterruptedException

public final native void notify()

当一个对象调用了wait()方法后,当前线程就会等待。

也就是说,如果有一个对象调用了wait()方法,那么该线程就会有就绪状态变为阻塞状态,一直等到其他线程调用了notfiy()方法将该线程恢复成就绪状态。

如果一个线程调用了object.wait(),那么它就会进入object对象的等待对列。这个等待对列中,可能会有多个线程,因为系统中的多个线程同事等待某一个对象。当object.notify()被调用时,它就会从等待对列中随机选择一个线程,并将其唤醒。这里的选择是不公平的,并不是先等待的线程后优先被选择,这个选择完全是随机的。

除了notify()方法外,Object对象还有一个notifyAll()方法,它和notify()的基本功能一致,但不同的是,它会唤醒在这个等待对列中所有等待的线程。

Object.wait()方法必须包含在对应的synchronized语句中,无论是wait()或者notify()都需要首项获得目标对象的一个监视器。

Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间,除了wait()方法可以被唤醒外,另外一个区别就是wait()方法会释放object的锁,并重新获得锁后,才能继续执行。

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

  • suspend可以使线程挂起。

  • resume可以使挂起的线程继续执行。

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

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

join等待某个线程结束后才会继续执行。在JDK中提供了如下的两个join()方法:


public final void join() throw InterruptedException

public final synchronized void join(long millis) throw InterruptedException

第一个没有参数的方法会无线等待,它一直会阻塞当前线程,直到目标线程执行完毕。

第二个方法给了一个最大的等待事件,如果超过了给定了的事件目标线程还没有执行完毕,当前线程不会再继续等待,而继续往下执行。

下面的是JDK中join()的实现的核心代码:


while (isAlive()) {

    wait(0);

}

Thread.yield() 线程让步,该线程让出CPU让其他线程使用,当然它只是让出了CPU,还会参与CPU的竞争。它的定义如下:


public static native void yield()

线程组

ThreadGroup 线程组,可以将创建的线程加入线程组。


public class ThreadGroupName implements Runnable {

    public static void main(String[] args) {

        ThreadGroup tg = new ThreadGroup("PrintGroup");

        Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");

        Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");

        t1.start();

        t2.start();

        System.out.println(tg.activeCount());

        // 打印出ThreadGroup中的相关信息

        tg.list();

    }

    public void run() {

        String groupAndName = Thread.currentThread().getThreadGroup().getName()

                + "-" + Thread.currentThread().getName();

    while (true) {

        System.out.println("I am " + groupAndName);

        try {

            Thread.sleep(3000);

        } catch (InterruptedException e) {

            e.printStackTrace();

            }

        }

    }

}

activeCount()可以获得活动线程的总数,但是由于线程是动态的,因此这个值是一个估计值,无法精确。

线程组还有一个stop()方法,它可以停止线程组中的所有线程,但是它的问题和Thread的stop()是一样的。

用户线程

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


public class DaemonDemo {

  public static class DaemonT extends Thread {

      @Override

      public void run() {

          while (true) {

              System.out.println("I am alive");

              try {

                  Thread.sleep(1000);

              } catch (InterruptedException e) {

                  e.printStackTrace();

              }

          }

      }

  }

  public static void main(String[] args) throws InterruptedException {



      Thread t = new DaemonT();



      t.setDaemon(true);



      t.start();





      Thread.sleep(2000);



  }

}

将一个线程设置为守护线程一定要在线程启动之前设置,如果线程已经启动了,如果设置的话那么就会抛出一个异常,如下:


Exception in thread "main" java.lang.IllegalThreadStateException

    at java.lang.Thread.setDaemon(Thread.java:1359)

    at top.mcwebsite.concurrency.start.DaemonDemo.main(DaemonDemo.java:26)

但是在这个线程依然能够执行,只是变成了一个普通的用户线程。

在上面的这个例子中,当主线程退出后,守护线程也随之结束了,如果只是一个普通的线程,那么它不会随主线程的结束而结束。

线程优先级

线程优先级越高,获得CPU的可能行就会越大,当然这里只是可能性。

在Java中,使用1到10表示线程的优先级,当然在实际的操作系统中不一定是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 th

*/

public final static int NORM_PRIORITY = 5;

/**

* The maximum priority that a thread can have.

*/

public final static int MAX_PRIORITY = 10;

你可能感兴趣的:(Java并发编程基础)