2 java并行基础

我们认真研究如何才能构建一个正确、健壮并且高效的并行系统。

进程与线程

进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程是线程的容器。程序是指令、数据和其组织形式的描述,进程是程序的实体。进程中可以容纳若干个线程。

进程和线程的关系:线程就是轻量级的进程,是程序执行的最小单位。为什么我们使用多线程而不是多进程?因为线程间的切换调度成本远远小于进程,所以我们使用多线程而不是多进程。

线程的生命周期

线程的所有状态都在Thread中的State枚举中定义。

public enum State{
  NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

NEW状态表示刚刚创建的线程,这种线程还没开始执行。start()方法调用时,线程开始执行。当线程执行时,处于RUNABLE状态,表示线程所需的一切资源都已经准备好了。

如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。

waitingtime_waiting都表示等待状态,它们的区别是waiting会进入一个无时间限制的等待,time_waiting会进行有时间限制的等待状态。一般说,waiting的线程是在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入runnable状态。当线程执行完毕后,则进入terminated状态,表示结束。

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

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

这节了解一下java为线程操作提供的一些API。

新建线程

新建线程很简单,一种可以使用继承Thread,重载run()方法来自定义线程,下面是匿名内部类,也是重载了run()方法:

Thread t1 = new Thread(){
     @Override
     public void run() {
       System.out.println("Hello, I am t1");
     }
};
t1.start();

start()后,线程Thread,有一个run()方法,start()方法会新建一个线程并让这个线程执行run()方法。

t1.start()和t1.run()两个方法的区别:start()会开启新的线程,并调用run()执行线程;直接调用run()方法也能通过编译,却不能新建线程,而是在当前线程中调用run()方法(不要尝试这样开启新线程,它只会在当前线程中,串行执行run()中的代码)。

Thread t1 = new Thread();
t1.run();

第二种是使用Runnable接口来实现同样的操作。这种方法解决了java单继承实现多线程的缺点:

public class CreateThread implements Runnable {

    @Override
    public void run() {
        System.out.println("Hi!I am Runnable");
    }

    public static void main(String args[]) {
        Thread thread = new Thread(new CreateThread());
        thread.start();
    }
}

终止线程

一般,线程在执行完毕就会结束,无需手动关闭。特殊情况,需要手动关闭。

使用stop关闭

可以使用stop关闭,但是不推荐,为什么?原因是stop()太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。举个例子:

记录1:ID=1,name=小明
记录2:ID=2,name=小王

上面数据库中要么是存记录1,要么存记录2,否则说明数据被损坏了,在单线程中不会出现这种情况,单在多线程中则会出现这样的情况。

Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而此时,线程写到一半便终止了。由于锁被释放,等待该锁的读线程也可以读到这个不一致的数据。如下图:

代码模拟:

public class StopThreadUnsafe {
    public static User user = new User();
    public static class User {
        private int id;
        private String name;
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public User() {
            id = 0;
            name = "0";
        }
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", id=" + id +
                    '}';
        }
    }
    public static class ChangeObjectThread extends Thread {
        public void run() {
            while (true) {
                synchronized (user) {
                    int v = (int) (System.currentTimeMillis() / 1000);
                    user.setId(v);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.setName(v + "");
                }
                Thread.yield();
            }
        }
    }
    public static class ReadObjectThread extends Thread {
        public void run() {
            while (true) {
                synchronized (user) {
                    if (user.getId() != Integer.parseInt(user.getName())) {
                        System.out.println(user.toString());
                    }
                }
                Thread.yield();
            }
        }
    }
    public static void main(String args[]) throws InterruptedException {
        new ReadObjectThread().start();
        while (true) {
            Thread thread = new ChangeObjectThread();
            thread.start();
            Thread.sleep(150);
            thread.stop();
        }
    }
}

上面程序本来应该输出id和name的值都相同,但是却输出如下的错误数据,这种错误没有报错,很难查找。

User{name='1565947644', id=1565947645}
User{name='1565947644', id=1565947645}

如何解决上面的问题?需要由我们自行决定线程何时退出。仍然用本例说明,只需要将ChangeObjectThread线程增加一个stopMe()即可:

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

    public void stopMe(){
        stopme = true;
    }

    public void run() {
        while (true) {
            //手动停止线程
            if (stopme){
                System.out.println("exit by stop me");
                break;
            }
            synchronized (user) {
                int v = (int) (System.currentTimeMillis() / 1000);
                user.setId(v);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                user.setName(v + "");
            }
            Thread.yield();
        }
    }
}

线程中断

线程中断可以和stop()一样起到退出线程的作用,但是它不会立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于是否退出由目标线程自行决定。

与线程中断的三个方法:

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

如果不手动加入中断处理的逻辑,即使对线程中断,这个中断也不会起任何作用。

public class T1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                while(true){
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("Interruted!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Interrupted when Sleep");
                        Thread.currentThread().interrupt();     //1
                    }
                    Thread.yield();

                }
            }
        };

        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
    }
}

如果去掉上例代码run方法中的Thread.sleep(2000),那么看起来和之前的stopme的方案很相似,但是中断的功能更强大。如果在循环体中,类似于wait()或者sleep()这样的操作,只能通过中断来识别。

若在线程休眠期间发生中断,它会抛出一个InterruptedException中断异常,并且清除中断标记。在上例代码中,1处是在捕获异常后(此时已清除了中断标记)重新设置中断标志,使其在下一次循环进入if语句中断循环。

等待(wait)和通知(notify)

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

当一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。直到等到其他线程调用了obj.notify为止。显然,这个对象成为了多个线程之间的有效通信手段。

wait()和notiry()如何工作?

如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程将其唤醒。这个选择完全是随机的。而object.notifyAll()会将这个等待队列中所有等待的线程唤醒,而不会随机一个。

Object.wait()方法不是随便调用的,必须包含在对应synchronzied语句中,无论是wait()或者notify()都需要首先获得目标对象的一个监视器。下图展示了wait()和notify()的工作流程。

下面代码简单的使用了wait()和notify(),T1执行了object.wait()方法,这时立马释放对象锁。此时正在等待对象锁的T2捕获到后,在2处执行object.notify()方法,但是此时和object.wait()方法不同,不立马释放,而是执行完synchronized块的代码后才释放。T2释放后,T1再次捕获,执行T2接下来的程序。

public class SimpleWN {
   final static Object object = new Object();
   public static class T1 extends Thread{
        public void run()
        {
            synchronized (object) {
                System.out.println(System.currentTimeMillis()+":T1 start! ");
                try {
                   System.out.println(System.currentTimeMillis()+":T1 wait for object ");
                   object.wait();  //1 wait()后,马上释放对象锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis()+":T1 end!");
            }
        }
   }
   public static class T2 extends Thread{
        public void run()
        {
            synchronized (object) {
                System.out.println(System.currentTimeMillis()+":T2 start! notify one thread");
                object.notify();    //2 notify()后,没有马上释放对象锁,而是执行完synchronized块的代码后释放
                System.out.println(System.currentTimeMillis()+":T2 end!");
                try {
               Thread.sleep(3000);
            } catch (InterruptedException e) {
            }
                System.out.println(System.currentTimeMillis()+":T2 after sleep!");
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(System.currentTimeMillis()+":T2 after synchronized!");
        }
   }
   public static void main(String[] args) {
        Thread t1 = new T1() ;
        Thread t2 = new T2() ;
//        Thread t1_1 = new T1() ;
//        t1_1.start();
        t1.start();
        t2.start();
   }
} /**
1566280793124:T1 start!
1566280793125:T1 wait for object
1566280793127:T2 start! notify one thread
1566280793127:T2 end!
1566280796127:T2 after sleep!
1566280796128:T1 end!
1566280797127:T2 after synchronized!
*/

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

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

被挂起的线程,必须要等待resume()后,才能继续执行。

这对方法已经不推荐使用了。不推荐的原因是因为suspend()在导致线程暂停的同时,并不会释放任何锁资源,直到等到resume()才释放。如果resume()操作意外出现在suspend()前面,就可能导致永久挂起。。此时,任何其他线程想要访问被它占用的锁时,都会受到牵连。甚至整个系统运行不正常。

public class BadSuspend {
   public static Object u = new Object();
   static ChangeObjectThread t1 = new ChangeObjectThread("t1");
   static ChangeObjectThread t2 = new ChangeObjectThread("t2");

   public static class ChangeObjectThread extends Thread {
      public ChangeObjectThread(String name){
         super.setName(name);
      }
      @Override
      public void run() {
         synchronized (u) {
            System.out.println("in "+getName());
            Thread.currentThread().suspend();   //1
         }
      }
   }

   public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        System.out.println("t1 resume!");
        t2.resume();
        System.out.println("t2 resume!");
        t1.join();
        t2.join();
    }
}/**
in t1
t1 resume!
t2 resume!
in t2
*/

上面程序中,t1.start()后线程走到1处被挂起,此时不释放对象锁。t2.start()后,t2需要等待t1释放的对象锁。在t2等待锁的过程中,t2.resume()已经发生了(通过打印结果可以看出),随后才发生t2的Thread.suspend()。这时,t2被永久挂起。

那如果需要一个比较可靠的suspend()函数,该怎么做呢?可以利用wait()和notify(),在应用层面实现suspend()和resume():

public class GoodSuspend {
    public static Object u = new Object();
    public static class ChangeObjectThread extends Thread {
        // 标记变量,表示当前线程是否被挂起
        volatile boolean suspendme = false;     //1
        // 挂起线程
        public void suspendMe(){
            suspendme = true;
        }
        // 继续执行线程
        public void resumeMe(){
            suspendme = false;
            synchronized (this){
                notify();
            }
        }
        @Override
        public void run(){
            while (true){
                synchronized (this){    //2
                    while (suspendme){
                        try {
                            wait();
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                }
                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(1000);

        t1.suspendMe();
        System.out.println("suspend t1 2 sec");
        Thread.sleep(2000);

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

在1处,给出了一个标记suspendme,表示当前线程是否被挂起,同时,增加了suspendMe()(通过执行wait()方法实现挂起)和resumeMe()(通过执行notify()通知继续执行,并清除挂起标记),注意,2处给自己加锁。

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

join

在多线程中,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行,jdk提供了join()来实现这个功能。有2个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.yield()

public static native void yield();

这是一个静态方法,它会让当前线程让出CPU。在让出CPU后,还会进行CPU资源的争夺,至于是否能再次分配,就不一定了。它的调用好像是在说:我已经完成了一些重要的工作了,我可以休息一下,给其他线程一些工作机会。

volatile与java内存模型(JMM)

之前提到过:java内存模型围绕原子性、有序性和可见性展开。

为了在适当的场合,确保线程间的有序性、可见性和原子性。java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,这个地方要特别注意,不能随意变动优化目标指令。关键字volatile就是其中之一。

使用volatile去申明一个变量,可以保证这个变量的可见性的特点。在之前的例子MultiThreadLong中,long型的 t 改为volatile,使其保证了原子性

public class MultiThreadLong {
   public volatile static long t=0;
   public static class ChangeT implements Runnable{
   ......

volatile对于保证操作的原子性有非常大的帮助,但是volatile并不能替代锁,它无法保证一些复合操作的原子性。如下例中,无法保证i++的原子操作

public class PlusTask implements Runnable {
    public volatile static Integer j = 0;   //1
    public void add(){
        for (int i = 0; i < 10000; i++) {
            j++;
        }
    }

    @Override
    public void run() {
//        synchronized (PlusTask.class) {
            add();
//        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        PlusTask task = new PlusTask();
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new PlusTask());
            threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
        System.out.println(j);
    }
}

上面的代码中,最终的值应该是100000,但实际总是会小于期望值。

volatile除了可以保证原子性,也能保证数据的可见性有序性。下面看一个例子:

public class NoVisibility {
    private static boolean ready;
    private static int number;

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

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

由于系统优化的结果,ReaderThread线程可能无法“看到”主线程的修改,导致ReaderThread永远无法退出,这是一个典型的可见性问题。但是,只要简单使用volatile来申明ready变量,告诉java虚拟机,这个变量可能会在不同的线程中修改。就可以解决问题了。

分门别类的管理:线程组

在一个系统中,如果线程数量很多,而且功能分配明确,就可以将相同功能的线程放置在一个线程组中,方便管理。

线程组的使用很简单:

public class ThreadGroupName implements Runnable {

   public static void main(String[] args) {
      ThreadGroup tg = new ThreadGroup("PrintGroup");   //1 建立名为“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()); //2
      tg.list();    //3
   }

   @Override
   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 (Exception e) {
            e.printStackTrace();
         }
      }
   }

}

代码1处,建立一个“PrintGroup”的线程组,并将T1和T2两个线程加入这个组中。2、3处,activeCount()可以获得活动线程的数量,list()打印线程组中所有线程信息。

线程组有一个stop(),它会停止线程组中所有的线程,但是和Thread.stop()会有相同的问题,要格外谨慎使用。

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

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

下面是一个简单的守护线程:

public class DaemonDemo {
    public static class DaemonT extends Thread{
        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);  //1
        t.start();

        Thread.sleep(2000); //当主线程执行完毕后,守护线程t也随之结束。
    }
}

1处将t设置成守护线程,当主线程执行完毕后,守护线程也随之结束。若不把t设置成守护线程,那么程序永远不会结束。

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

java中线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,当然,这是一个概率问题。这种优先级产生的后果不容易预测,优先级低的线程可能会导致饥饿现象(即使是优先级低,但是也不能饿死它啊)。因此,在要求严格的场合,还是需要自己在应用层解决线程调度问题。

在java中,使用1到10表示线程优先级。一般可以使用内置的三个静态变量标量表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

数字越大则优先级越高,但有效范围在1到10之间。例子:

public class PriorityDemo {
    public static class HightPriority extends Thread{
        static int count=0;
        public void run(){
            while(true){
                synchronized(PriorityDemo.class){
                    count++;
                    if(count>10000000){
                        System.out.println("HightPriority is complete");
                        break;
                    }
                }
            }
        }
    }
    public static class LowPriority extends Thread{
        static int count=0;
        public void run(){
            while(true){
                synchronized(PriorityDemo.class){
                    count++;
                    if(count>10000000){
                        System.out.println("LowPriority is complete");
                        break;
                    }
                }
            }
        }
    }

    /**
     * HightPriority先完成的次数多,但是 不保证
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Thread high=new HightPriority();
        LowPriority low=new LowPriority();
        high.setPriority(Thread.MAX_PRIORITY);  //1
        low.setPriority(Thread.MIN_PRIORITY);   //2
        low.start();
        high.start();
    }
}

上述代码中1、2处设置了线程的优先级,所以总是高优先级的线程执行得会快些。

线程安全的概念与synchronized

并发程序开发的一大关注重点就是线程安全。程序并行化是为了获得更高的执行效率,同时保证程序的正确性。因此,线程安全是并行程序的根本和根基。

public class AccountingVol implements Runnable {
    static AccountingVol accountingVol = new AccountingVol();
    static volatile int i = 0;

    public static void increase() {
        i++;
    }
    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(accountingVol);
        Thread t2 = new Thread(accountingVol);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代码中,线程t1、t2可能同时读取i为0,并各自计算得到i=1,并先后写入这个结果,因此,虽然i++被执行了2次,但实际i的值只增加了1。

要解决这个问题,我们就要保证多个线程对i进行操作时完全同步。就是说,当线程A在写入时,B不仅不能写,也不能读。java中,提供了一个重要的关键字synchronized来实现这个功能。

synchronized

作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每次只能有一个线程进入同步块。

用法:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

指定加锁对象

下面程序中,将synchronized作用于给定对象instance。每次进入被synchronized包裹的代码段,都会请求instance的锁。若有其他线程占用,则必须等待。

public class AccountingSync implements Runnable{
   static AccountingSync instance=new AccountingSync();
   static int i=0;
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         synchronized(instance){
            i++;
         }
      }
   }
   //main程序见上例代码
}

直接作用于实例方法

public class AccountingSync2 implements Runnable{
   static AccountingSync2 instance=new AccountingSync2();
   static int i=0;
   public synchronized void increase(){
      i++;
   }
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         increase();
      }
   }
   public static void main(String[] args) throws InterruptedException {
      AccountingSync2 i1=new AccountingSync2();
//    AccountingSync2 i2=new AccountingSync2();
      Thread t1=new Thread(i1);
      Thread t2=new Thread(i1);
      t1.start();t2.start();
      t1.join();t2.join();
      System.out.println(i);
   }
}

上例代码中,synchronized关键字作用于一个实例方法,这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在此例中,线程t1和t2需要用到相同的Ruanable实例i1,这样才能关注到同一个对象锁上。若两个线程使用不同的两个Runnable实例t1,t2,即两个线程使用了两把不同的锁。

但是,我们可以把increase()方法改成static的,这样方法块请求的是当前类的锁,而不是当前实例的,因此,线程可以同步。如下:

public class AccountingSync2 implements Runnable{
   static AccountingSync2 instance=new AccountingSync2();
   static int i=0;
   public static synchronized void increase(){  //3
      i++;
   }
   @Override
   public void run() {
      for(int j=0;j<10000000;j++){
         increase();
      }
   }
   public static void main(String[] args) throws InterruptedException {
      AccountingSync2 i1=new AccountingSync2();
      AccountingSync2 i2=new AccountingSync2();
      Thread t1=new Thread(i1);     //1
      Thread t2=new Thread(i2);     //2
      t1.start();t2.start();
      t1.join();t2.join();
      System.out.println(i);
   }
}

1和2处使用了两个不同的Runable实例,但是3处的同步方法为static的,此方法需要的是当前类的锁而非当前实例的锁,因此线程间可以正确同步。

除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。被synchronized限制的多个线程死串行执行的。

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

有异常的异常堆栈好修复,但是,没有异常、没有日志、没有堆栈的异常,就很让人抓狂了。

无提示的错误案例

如果你运行下面的程序,会发现一个隐藏的错误:

int v1 = 1073741827;
int v2 = 1431655768;
int ave = (v1+v2)/2;

把上面ave打印出来,会发现ave的值是-894784850,一个负数。那是因为溢出。这种隐形的错误很难找,和幽灵一般。

并发下的ArrayList

ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错,那会出现哪些问题呢?

public class ArrayListMultiThread {
    static ArrayList<Integer> al = new ArrayList<Integer>(10);
    public static class AddThread implements Runnable{
        @Override
        public void run() {
            for (int i=0;i<10000000;i++){
                al.add(i);
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AddThread());
        Thread t2 = new Thread(new AddThread());
        t1.start();t2.start();
        t1.join(); t2.join();
        System.out.println(al.size()); //抛出异常 返回小于2000的数值。
    }
}

在上面这段错误的代码中,原本输出的应该是20000000,但是由于ArrayList不支持,我们可能会得到3中不同的结果:

  • 第一,程序正常结束,几率极小
  • 第二,程序抛出异常:
    Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 1823230
      at java.util.ArrayList.add(ArrayList.java:459)
      at geym.ch2.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:11)
      at java.lang.Thread.run(Thread.java:745)

    这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另一个线程访问到了不一致的内部状态,导致了越界问题。

  • 第三,出现一个非常隐蔽的问题,打印的值小于期望值20000000。

!!改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

并发下诡异的HashMap

HashMap同样不是线程安全的。

public class HashMapMultiThread {
    static Map<String, String> map = new HashMap<String, String>();

    public static class AddThread implements Runnable {
        int start = 0;

        public AddThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i += 2) {
                map.put(Integer.toString(i), Integer.toBinaryString(i));
            }
        }

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

            // 根据你的电脑CPU核数来配置 两核启两个线程就行
            Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
            Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(map.size());
        }
    }
}

我们期望得到100000,但是,实际可能会有三种情况:

  • 第一,程序正常结束,大小为预期值。
  • 第二,程序正常结束,但是小于100000。
  • 第三,程序永远无法结束

前面两种情况和ArrayList类似,对于第三种情况,由于多线程的冲突,HashMap中的Entry<K,V>链表的结构遭到破坏,链表成环了!当链表成环时,HashMap.put()方法中的迭代就等于死循环。如图,展示了最简单的环状结构,key1和key2互为对方的next元素。

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

public class BadLockOnInteger implements Runnable {
    public static Integer i = 0;
    public static BadLockOnInteger instance = new BadLockOnInteger();
    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            synchronized (i){   //1
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

上面代码1处,把锁加在了i上,似乎并没有什么问题,然而,我们运行程序,却得到了比预期值20000000要小的数,这是为什么呢?因为Integer属于不可变对象。就是说Integer的值不能被修改,如果要修改,就要新建一个Integer对象。这样在多个线程间,并不一定能够看到同一个i对象(i一直在变),每次加锁都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。

修正这个问题,只要把锁加在instance上就可以了:

synchronized (i){

改为下面代码即可:

synchronized (instance){

原文地址:https://www.cnblogs.com/sean-zeng/p/11422297.html

时间: 2024-08-29 03:40:28

2 java并行基础的相关文章

JAVA并行程序基础

JAVA并行程序基础 一.有关线程你必须知道的事 进程与线程 在等待面向线程设计的计算机结构中,进程是线程的容器.我们都知道,程序是对于指令.数据及其组织形式的描述,而进程是程序的实体. 线程是轻量级的进程,是程序执行的最小单位.(PS:使用多线程去进行并发程序的设计,是因为线程间的调度和切换成本远小于进程) 线程的状态(Thread的State类): NEW–刚刚创建的线程,需要调用start()方法来执行线程: RUNNABLE–线程处于执行状态: BLOCKED–线程遇到synchroni

Java 并发基础

Java 并发基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及部分运行时环境,因此编程时需要小心,确保线程不会妨碍同一进程中的其他线程; 多线程优势 进程之间不能共享内存,但线程之间共享内存/文件描述符/进程状态非常容易. 系统创建进程时需要为该其分配很多系统资源(如进程控制块),但创建线程的开销要小得多,因此线程实现多任务并发比进程效率高. Java语言内置多线程支持,而不是单纯采

Java多线程基础

1. 前言 这篇文章,是对Java多线程编程的基础性介绍. 文章将介绍Java语言为支持多线程编程提供的一些特性.通过这篇文章,您将了解到如何通过Java语言创建一个线程,如何通过内置的锁来实现线程间的同步,如何在线程间进行通信以及线程的中断机制. 2. 什么是线程 线程是操作系统调度的最小单位,在一个进程中,一般至少有一个线程在运行.一个进程中包含的多个线程,在多核处理器中,操作系统可以将多个线程调度到不同的CPU核心上运行,多个线程可以并行运行. 在同一个进程中的多个线程,共享同一个进程空间

Java多线程基础总结

背景 Java采用多线程方式实现并行计算,当然并行计算也可以采用多进程方式实现,但是进程切换耗费比较高.而且进程间是隔离的,进程间通信机制比较麻烦,最后JVM本身在操作系统中就一个进程,由它再启动一个进程不太合适,所以Java采用多线程方式实现并行计算. Java从诞生之初,多线程就围绕的是Runnable接口和Thread类展开的.它的底层采用的是c的p线程方式,而且由于多线程的复杂性,p线程的很多概念知识被延伸到了Java层面,这对Java开发者来说算是一个不幸的消息.但是由于多线程的复杂性

Java 并发基础常见面试题总结

Java 并发基础常见面试题总结 1. 什么是线程和进程? 1.1. 何为进程? 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的.系统运行一个程序即是一个进程从创建,运行到消亡的过程. 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程. 如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行). 1.2

Java语法基础

Java语法基础 1.  关键字 某些单词对编译器有着特殊的含义,并且不能作为标示符使用,全部是小写字母 Java语言关键字 abstract boolean break byte case catch char class try do default continue double else extends assert final finally float for If implement import instanceof int interface long native new g

【Java面向对象基础(三)】面向对象思想

[喵"的Android之路][基础篇(三)][Java面向对象基础]面向对象思想 1 面向对象的WWH 1.1 What--什么是面向对象 首先,要理解“对象”.在Thinking in Java中提到“Everything is an object”,即万物皆对象.这里的“对象”指的是任何实体或者任何可以当作实体看待的“虚幻的事物”.比如现实中的人(男人.女人.老人.小孩而...).动物(大象.狮子.猴子...).桌子.凳子.汽车.ATM机等等,也可以使看不见摸不着的空气(氧气.二氧化碳.氮气

java并行调度框架封装及示例

参考资料:  阿里巴巴开源项目 CobarClient  源码实现. 分享作者:闫建忠 分享时间:2014年5月7日 --------------------------------------------------------------------------------------- 并行调度封装类设计: BXexample.java package org.hdht.business.ordermanager.quartzjob; import java.util.ArrayList;

java 集合基础1 学习笔记

集合特点: 1.用于存储对象的容器. 2.集合的长度是可变的. 3.集合中不可以存储基本数据类型值. Collection接口常见方法: 1.添加 boolean add(obj); boolean addAll(Collection coll); 2.删除 boolean remove(obj); boolean removeAll(Collection coll); void clear();//清空集合 3.判断 boolean contains(obj); boolean contain