第2章 Java并行程序基础(二)

2.3 volatile 与 Java 内存模型(JMM)

  • volatile对于保证操作的原子性是由非常大的帮助的(可见性)。但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操 作的:
static volatile int i = 0;
public static class PlusTask implements Runable {
    @Override
    public void run() {
        for (int k = 0; k < 10000; k++) {
            i++;
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[10];
    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(i);
}
  • 上述代码的输出总是会小于100000,由于i++不是原子性的。
  • 此外,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(1000);
    }
}
  • 在虚拟机的Client模式下,由于JIT并没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但是在Server模式下,由于系统优化的结果,ReaderThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出。
  • 注意:可以使用Java虚拟机参数-server切换到Server模式。

2.4 分门别类的管理:线程组

  • 在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。
  • 线程组的使用非常简单,如下:
public class ThreadGroupName implements Runnable {
    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("PrintGroup");
        //使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。
        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());
        //打印这个线程组中所有的线程信息。
        tg.list();
    }
    
    @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 (InterruptedException e) {
                e.printStaceTrace();
            }
        }
    }
}

2.5 驻守后台:守护线程(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); //将线程t设置为守护线程。必须在start()之前设置。
        t.start();
        
        Thread.sleep(2000);
    }
}
  • 由于t被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,main线程结束后,t线程还会不停地打印,永远不会结束。

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

  • 在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1;
public final static int MIN_PRIORITY = 5;
public final static int MIN_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;
                    }
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Thread high = new HightPriority();
        LowPriority low = new new LowPriority();
        high.setPriority(Thread.MAX_PRIORITY);
        low.setPriority(Thread.MIN_PRIORITY);
        low.start();
        high.start();
    }
}
  • 在对count累加前,我们使用synchronized产生了一次资源竞争。目的是使得优先级的差异表现得更为明显。

2.7 线程安全的概念与synchronized

  • 关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
  • 关键字synchronized可以有多种用法。
    • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
    • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
    • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
  • 下述代码,将synchronized作用于一个给定对象instance,因此,每次当线程进入被synchronized包裹的代码段,就都会要求请求instance实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就必须等待。这样,就保证了每次只能有一个线程执行i++操 作。
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++;
            }
        }
    }
}
  • 当然,上述代码也可以写成如下形式,两者是等价的:
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) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
  • 上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance)这样才能保证两个线程在工作时,能够关注到同一对象锁上去,从而保证线程安全。
  • 一种错误的同步方式如下:
public class AccountingSyncBad implements Runnable {
    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) {
        Thread t1 = new Thread(new AccountingSyncBad());
        Thread t2 = new Thread(new AccountingSyncBad());
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
  • 上述代码中,这两个线程的Runnable实例并不是同一个对象。使用的是两把不同的锁。因此,线程安全是无法保证的。
  • 但是我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。
public static synchronized void increase() {
    i++;
}
  • 这样即使两个线程指向不同的Runable对象,但由于方法块需要请求的是当前类的锁,而非当前实例,因此,线程间还是可以正确同步。
  • 除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决。
posted @ 2018-01-19 21:59  _sanjun  阅读(245)  评论(0编辑  收藏  举报