zoukankan      html  css  js  c++  java
  • Java:多线程(二)

    与线程生命周期相关的方法:

    sleep 调用sleep方法会进入计时等待状态,等待时间到了,进入就绪状态。

    yield 调用yield方法会让别的线程执行,但是不确保真正让出。较少使用,官方注释都说 It is rarely appropriate to use this method

    join 调用join方法,会等待该线程执行完毕后才执行别的线程。

    interrupt 之前有stop方法可以中断线程,但是已经过时了。现在没有强制线程终止的方法。interrupt用来请求终止线程。

    wait/notify通知机制可以用来实现线程间通信。wait表示线程的等待,调用该方法会导致线程阻塞,直到另一线程调用notify或notifyAll方法才可令其执行。

    经典的生产者、消费者模式即是使用wait/notify机制得以完成。

    多进程和多线程有哪些区别呢?本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。

    没有可以强制线程终止的方法。然而,interrupt 方法可以用来请求终止线程。当对一个线程调用 interrupt 方法时,线程的中断位置将被置位。这是每一个线程都具有的 boolean 标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。

            Runnable r = () -> {
                try {
                    // ...
                    while (!Thread.currentThread().isInterrupted()) {
                        // do more work
                    }
                } catch (InterruptedException e) {
                    // thread was interrupted during sleep or wait
                } finally {
                    // clean up, if required
                }
                // exiting the run method terminates the thread
            };

     如果在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法),isInterrupted 检测既没必要也无用。如果在中断状态被置位时调用 sleep 方法,它不会休眠。相反,它将清楚这一状态并抛出 InterruptedException。因此, 如果你的循环调用 sleep,不会检测中断状态。相反,要如下所示捕获 InterruptedException 异常。

            Runnable r = () -> {
                try {
                    // ...
                    while ( more work to do ) {
                        // do more work
                        Thread.sleep(delay);
                    }
                } catch (InterruptedException e) {
                    // thread was interrupted during sleep or wait
                } finally {
                    // clean up, if required
                }
                // exiting the run method terminates the thread
            };

    Thread 中 interrupt( ) 方法向线程发送中断请求。线程的中断状态将被设置为 true。如果目前该线程被一个 sleep 调用阻塞,那么,InterruptedException 异常被抛出。 

    一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级。

    下面是一个案例,用户在银行中进行转账,每次转账都新建一个线程。如果线程安全,每次转账后银行账户余额都是相同的。

    这是 Bank 类,里面有转账的方法。

    import java.util.Arrays;
    
    public class Bank {
    
        private final double[] accounts;
    
        public Bank(int n, double initialBalance) {
            accounts = new double[n];
            Arrays.fill(accounts, initialBalance);
        }
    
        public void transfer(int from, int to, double amount) {
            if (accounts[from] < amount) {
                return;
            }
            System.out.println(Thread.currentThread().getName());
            accounts[from] -= amount;
            System.out.printf(" %14.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        }
    
        public double getTotalBalance() {
            double sum = 0;
            for (double a : accounts) {
                sum += a;
            }
            return sum;
        }
    
        public int size() {
            return accounts.length;
        }
    
    }

     测试类,启动多线程。

    public class UnsynchBankTest {
    
        public static final int NACCOUNTS = 100;
        public static final double INITIAL_BALANCE = 1000;
        public static final double MAX_AMOUNT = 1000;
        public static final int DELAY = 10;
    
        public static void main(String[] args) {
            final Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
            for (int i = 0; i < NACCOUNTS; i++) {
                final int fromAccount = i;
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            while (true) {
                                int toAccount = (int) (bank.size() * Math.random());
                                double amount = MAX_AMOUNT * Math.random();
                                bank.transfer(fromAccount, toAccount, amount);
                                Thread.sleep((int) (DELAY * Math.random()));
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                Thread t = new Thread(r);
                t.start();
            }
        }
    
    }

     银行总共 100 个账户,每个账户 1000 元,银行总额是 100000 元。启动代码后可以看到:

    Thread-1
    Thread-2
    Thread-0
    Thread-4
    Thread-6
    Thread-5
             790.94 from 2 to 4Thread-37
    Thread-36
             979.74 from 36 to 39Thread-35
             104.57 from 35 to 37Thread-34
             358.38 from 34 to 34Thread-33
             865.67 from 33 to 14Thread-32
              96.28 from 32 to 2 Total Balance:   96794.75
    Thread-31
             731.15 from 31 to 69 Total Balance:   96794.75

     可以看到运行一段时间后, 银行总额已经发生了错误。产生问题的原因是 transfer 方法执行过程中可能会被中断。如果能够确保线程在失去控制之前方法运行完成,那么银行账户对象的状态永远不会出现错误。

     有两种机制防止代码受到并发访问的干扰:

    • 关键字 synchronized
    • 启用 ReentrantLock 类 since 1.5

    首先采用 ReentrantLock 锁。

    修改 Bank 的代码如下:

    public class Bank {
    
        // ReentrantLock implements the Lock interface
        private Lock bankLock = new ReentrantLock(); 
    
        ...
    
        public void transfer(int from, int to, double amount) {
            bankLock.lock();
            try {
                System.out.println(Thread.currentThread().getName());
                accounts[from] -= amount;
                System.out.printf(" %14.2f from %d to %d", amount, from, to);
                accounts[to] += amount;
                System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            } finally {
                bankLock.unlock();
            }
        }
    }

     运行结果后如下。

    Thread-1
             859.57 from 1 to 47 Total Balance:  100000.00
    Thread-66
             771.69 from 66 to 34 Total Balance:  100000.00
    Thread-2
             463.58 from 2 to 29 Total Balance:  100000.00
    Thread-0
             444.86 from 0 to 93 Total Balance:  100000.00

    假定一个线程调用 transfer,在执行结束前被剥夺了运行权。假定第二个线程也调用 transfer,由于第二个线程不能获得锁,将在调用 lock 方法时被阻塞。它必须等待第一个线程完成 transfer 方法的执行之后才能再度被激活。当第一个线程释放锁时,那么第二个线程才能开始运行。

    锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

    例如,transfer 方法调用 getTotalBalance 方法,这也会封锁 bankLock 对象,此时 bankLock 对象的持有计数为 2。当 getTotalBalance 方法退出的时候,持有计数变回 1。当 transfer 方法退出的时候,持有计数变为 0。线程释放锁。

    条件对象

    通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

    仍然接上面的例子,当账户中没有足够的余额时,应该做什么呢?等待直到另一个线程向账户中注入了资金。但是,这一线程刚刚获得了对 bankLock 的排它性访问,因此别的线程没有进行存款操作的机会。这就是为什么我们需要条件对象的原因。

    public class Bank {
    
        private final double[] accounts;
        // ReentrantLock implements the Lock interface
        private Lock bankLock; 
        private Condition sufficientFunds;
    
        public Bank(int n, double initialBalance) {
            accounts = new double[n];
            Arrays.fill(accounts, initialBalance);
            bankLock = new ReentrantLock();
            sufficientFunds = bankLock.newCondition();
        }
    
        public void transfer(int from, int to, double amount) {
            bankLock.lock();
            try {
                while (accounts[from] < amount) {
                    sufficientFunds.await();
                }
                System.out.println(Thread.currentThread().getName());
                accounts[from] -= amount;
                System.out.printf(" %14.2f from %d to %d", amount, from, to);
                accounts[to] += amount;
                System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
                sufficientFunds.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                bankLock.unlock();
            }
        }
            ...
    }

    等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 signalAll 方法为止。

    当另一个线程转账时,它应该调用 sufficientFunds.await( ); 这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继续执行。

    此时,线程应该再次测试该条件。由于无法确保该条件被满足,signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。

    至关重要的是最终需要某个其他线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永不再运行了。这将导致死锁(deadlock)现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法,那么它也被阻塞。没有任何线程可以在解除其他线程的阻塞,那么该程序就挂起了。

    应该何时调用 signalAll 呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用 signalAll。例如,当一个账户余额发生改变时,等待线程会应该有机会检查余额。

    总结一下有关锁和条件的关键之处:

    • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
    • 锁可以管理试图进入被保护代码段的线程
    • 锁可以拥有一个或多个相关的条件对象
    • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

    关键字 synchronized 的使用

    如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。

    public synchronized void method(){
        // method body
    }

    上面等价于

    public void method(){
        this.intrinsicLock.lock();
        try {
            // method body
        } finally {
            this.intrinsicLock.unlock();
        }
    }

    例如,可以简单地声明 Bank 类的 transfer 方法为 synchronized,而不是使用一个显式的锁。

    内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集,notifyAll / notify 方法解除等待线程的阻塞状态。换句话说,调用 wait 或 notifyAll 等价于 intrinsicCondition.await(); intrinsicCondition.signalAll();

    使用 synchronized 关键字来编写代码要简洁得多。当然,要理解这一代码,你必须了解每一个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入 synchronized 方法的线程,由条件来管理那些调用 wait 的线程。

    将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关类对象的内部锁。例如,如果 Bank 类有一个静态同步的方法,那么当该方法被调用时,Bank.class 对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。

    代码中锁的运用?

    • 最好既不使用 Lock / Condition 也不使用 synchronized 关键字。在许多情况下你可以使用 java.util.concurrent 包中的一种机制,它会为你处理所有的加锁
    • 如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写代码数量,减少出错的几率
    • 如果特别需要 Lock / Condition 结构提供的独有特性时,才使用 Lock / Condition

    监视器 monitor 可以看做是经过特殊布置的建筑,这个建筑有一个特殊的房间,该房间通常包含一些数据和代码,但是一次只能一个消费者(thread)使用此房间。

    当一个消费者(线程)使用了这个房间,首先他必须到一个大厅(Entry Set)等待,调度程序将基于某些标准(e.g.FIFO)将从大厅中选择一个消费者(线程),进入特殊房间,如果这个线程因为某些原因被挂起,它将被调度程序安排到等待房间,并且一段时间之后会被重新分配到特殊房间,按照上面的线路,这个建筑物包含三个房间,分别是特殊房间、大厅以及等待房间。 

    监视器用来监视线程进入这个特别房间,他确保同一时间只能有一个线程访问特殊房间中的数据和代码。

    在 Java 虚拟机中,每个对象 Object 和 Class 通过某种逻辑关联监视器,为了实现监视器的互斥功能,每个对象都关联着一个锁(有时也叫互斥量,在操作系统书籍中称为信号量), 

     为了使数据不被多个线程访问,Java 提供了同步块以及同步方法两种实现,一旦一段代码被嵌入到一个 synchronized 关键字中,意味着放入了监视区域,JVM 在后台会自动为这段代码实现锁的功能。

    线程的局部变量

    避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。

    例如,SimpleDateFormat 类是线程不安全的。要为每个线程构造一个实例,可以使用以下代码:

        private static final ThreadLocal<SimpleDateFormat> dateFormat =
                ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    
        public static void main(String[] args) {
            SimpleDateFormat simpleDateFormat = dateFormat.get();
            String dateStamp = simpleDateFormat.format(new Date());
            System.out.println(dateStamp);
        }

    其中 withInitial ( Supplier <? extends S> supplier ) 创建一个线程局部变量,其初始值通过调用给定的 supplier 生成。而 get ( ) 方法得到这个线程的当前值。如果是首次调用 get,会调用 initialize 来得到这个值。

    参考:

    https://segmentfault.com/a/1190000014463417

    多线程三分钟就可以入个门了

    https://segmentfault.com/a/1190000014741369

    https://blog.csdn.net/wthfeng/article/details/78762343

    Java 核心技术 卷1

  • 相关阅读:
    IDOC练习(一、发送端配置)
    节约性能的一个sql
    获取用户IP地址及用户名
    获取一个结构的所有字段
    获取文件名称的两个函数
    时间戳
    SMOD和CMOD
    内表生成XML简单实例
    ABAP&nbsp;编辑器设置(主动…
    ABAP常用Function
  • 原文地址:https://www.cnblogs.com/colin220/p/10277594.html
Copyright © 2011-2022 走看看