zoukankan      html  css  js  c++  java
  • 第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限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决。
  • 相关阅读:
    原创:微信小程序页面跳转展示缓冲提示
    转发:微信小程序-template模板使用
    JS正则判断输入框是否仅仅含有汉字、字母和数字
    jQuery使用正则判断是否含有非法字符
    允许远程用户登录访问mysql的方法
    如何使php页面中不再出现NOTICE和DEPRECATED的错误提示
    原生php如何获取当前页面的url
    jQuery写缓存之:sessionStorage的运用,配合PHP将不同tab页的数据写入后台
    TP2.0或3.1 或者 3.2 下使用ajax+php做无刷新分页(转+自创)
    jquery中的replaceWith()和html()的区别
  • 原文地址:https://www.cnblogs.com/sanjun/p/8319008.html
Copyright © 2011-2022 走看看