美文网首页
《实战高并发程序设计》读书笔记-线程基本知识补充

《实战高并发程序设计》读书笔记-线程基本知识补充

作者: 乙腾 | 来源:发表于2021-05-03 11:07 被阅读0次

    volatile

      Java内存模型都是围绕着原子性、有序性和可见性展开的,为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一,用于保持线程之间的\color{red}{可见性}

      但是同时应该注意的是,volatile并不能当锁用,他无法保证共享变量操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操作的:

    01 static volatile int i=0;
    02 public static class PlusTask implements Runnable{
    03     @Override
    04     public void run() {
    05         for(int k=0;k<10000;k++)
    06             i++;
    07     }
    08 }
    09
    10 public static void main(String[] args) throws InterruptedException {
    11     Thread[] threads=new Thread[10];
    12     for(int i=0;i<10;i++){
    13         threads[i]=new Thread(new PlusTask());
    14         threads[i].start();
    15     }
    16     for(int i=0;i<10;i++){
    17         threads[i].join();
    18     }
    19
    20     System.out.println(i);
    21 }
    

      执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000(10个线程各累加10000次)。但实际上,上述代码的输出总是会小于100000。

    重点

    \color{red}{只能保证线程之间的可见性,和程序的有序性,不能保证共享变量操作的原子性}。

    线程组

    可以通过线程组对不同的线程分批管理

    比如:

    01 public class ThreadGroupName implements Runnable {
    02     public static void main(String[] args) {
    03         ThreadGroup tg = new ThreadGroup("PrintGroup");
    04         Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
    05         Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
    06         t1.start();
    07         t2.start();
    08         System.out.println(tg.activeCount());
    09         tg.list();
    10     }
    11
    12     @Override
    13     public void run() {
    14         String groupAndName=Thread.currentThread().getThreadGroup().getName()
    15                 + "-" + Thread.currentThread().getName();
    16         while (true) {
    17             System.out.println("I am " + groupAndName);
    18             try {
    19                 Thread.sleep(3000);
    20             } catch (InterruptedException e) {
    21                 e.printStackTrace();
    22             }
    23         }
    24     }
    25 }
    

    通过\color{red}{ThreadGroup}创建一个名为“PrintGroup”的线程组,并将T1和T2两个线程加入这个组中。第8、9两行,展示了线程组的两个重要的功能,activeCount()可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确,list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助。代码中第4、5两行创建了两个线程,使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。
    线程组还有一个值得注意的方法stop(),它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()相同的问题(终止业务逻辑没有处理完的线程),因此使用时也需要格外谨慎。

    重点

    通过ThreadGroup为线程分组,并且为之按照业务需要命名,让线程的名字更有意义,慎用线程组的stop(),这会导致破坏线程业务逻辑的原子性, 线程组的activeCount()可以获得活动线程的总数,并且可以通过list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助

    守护线程(Daemon)

      守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。\color{red}{与之相对应的是用户线程,用户线程可以认为是系统的工作线程},它会完成这个程序应该要完成的业务操作。\color{red}{如果用户线程全部结束,这也意味着这个程序实际上无事可做了}。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,\color{red}{当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出}

    01 public class DaemonDemo {
    02     public static class DaemonT extends Thread{
    03         public void run(){
    04             while(true){
    05                 System.out.println("I am alive");
    06                 try {
    07                     Thread.sleep(1000);
    08                 } catch (InterruptedException e) {
    09                     e.printStackTrace();
    10                 }
    11             }
    12         }
    13     }
    14     public static void main(String[] args) throws InterruptedException {
    15         Thread t=new DaemonT();
    16         t.setDaemon(true);
    17         t.start();
    18
    19         Thread.sleep(2000);
    20     }
    21 }
    

      上述代码第16行,将线程t设置为守护线程。这里注意,\color{red}{设置守护线程必须在线程start()之前设置},否则你会得到一个类似以下的异常,告诉你守护线程设置失败。但是你的程序和线程依然可以正常执行。只是被当做用户线程而已。因此,如果不小心忽略了下面的异常信息,你就很可能察觉不到这个错误。

    Exception in thread "main" java.lang.IllegalThreadStateException
        at java.lang.Thread.setDaemon(Thread.java:1367)
        at geym.conc.ch2.daemon.DaemonDemo.main(DaemonDemo.java:20)
    

      在这个例子中,由于t被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,main线程结束后,t线程还会不停地打印,永远不会结束。

    重点

    设置守护线程必须在线程start()之前设置,否则会得到以下异常,导致守护线程失效,但是不影响主线程执行。

    java.lang.IllegalThreadStateException
    

    线程优先级

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

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

    数字越大则优先级越高,但有效范围在1到10之间。下面的代码展示了优先级的作用。高优先级的线程倾向于更快地完成。

    01 public class PriorityDemo {
    02     public static class HightPriority extends Thread{
    03         static int count=0;
    04         public void run(){
    05             while(true){
    06                 synchronized(PriorityDemo.class){
    07                     count++;
    08                     if(count>10000000){
    09                         System.out.println("HightPriority is complete");
    10                         break;
    11                     }
    12                 }
    13             }
    14         }
    15     }
    16     public static class LowPriority extends Thread{
    17         static int count=0;
    18         public void run(){
    19             while(true){
    20                 synchronized(PriorityDemo.class){
    21                     count++;
    22                     if(count>10000000){
    23                         System.out.println("LowPriority is complete");
    24                         break;
    25                     }
    26                 }
    27             }
    28         }
    29     }
    30
    31     public static void main(String[] args) throws InterruptedException {
    32         Thread high=new HightPriority();
    33         LowPriority low=new LowPriority();
    34         high.setPriority(Thread.MAX_PRIORITY);
    35         low.setPriority(Thread.MIN_PRIORITY);
    36         low.start();
    37         high.start();
    38     }
    39 }
    

      上述代码定义两个线程,分别为HightPriority设置为高优先级,LowPriority为低优先级。让它们完成相同的工作,也就是把count从0加到10000000。完成后,打印信息给一个提示,这样我们就知道谁先完成工作了。这里要注意,在对count累加前,我们使用synchronized产生了一次资源竞争。目的是使得优先级的差异表现得更为明显。
      大家可以尝试执行上述代码,可以看到,高优先级的线程在大部分情况下,都会首先完成任务(就这段代码而言,试运行多次,HightPriority总是比LowPriority快,但这不能保证在所有情况下,一定都是这样)。

    重点

    可以通过通过setPriority()设置线程的优先级,从0到10,优先级越高越容易抢占资源,但这不是绝对的,只是说理论上抢占共享资源的几率更高。

    synchronized

      Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。

      并发编程里两大核心问题——互斥和同步,都可以由管程来帮你解决。学好管程,理论上所有的并发问题你都可以解决,并且很多并发工具类底层都是管程实现的,所以学好管程,就是相当于掌握了一把并发编程的万能钥匙。

      关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性

    synchronized的多种用法

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

    以上是原书给的分类,但是我感觉可以按照锁对象这个维度来看synchronized的用法:

    锁对象

    锁实例对象
    1.修饰普通方法,锁的是this
    image.png

    此时锁对象是this,该类的实例对象。

    如果实例对象不是一个,那么保证不了共享资源的安全。

    2.synchronized(this)代码块
    image.png
    类锁,锁的是内存中唯一存在的class对象
    1.修饰静态方法
    image.png
    2.synchronized(*.class)代码块
    image.png

    synchronized的配套使用

    image.png image.png

    synchronized的使用例子

    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++;
                }
            }
        }
    

    当然,上述代码也可以写成如下形式,两者是等价的:

    01 public class AccountingSync2 implements Runnable{
    02     static AccountingSync2 instance=new AccountingSync2();
    03     static int i=0;
    04     public synchronized void increase(){
    05         i++;
    06     }
    07     @Override
    08     public void run() {
    09         for(int j=0;j<10000000;j++){
    10             increase();
    11         }
    12     }
    13     public static void main(String[] args) throws InterruptedException {
    14         Thread t1=new Thread(instance);//锁的是当前实例,但是两个线程是同一个锁对象
    15         Thread t2=new Thread(instance);
    16         t1.start();t2.start();
    17         t1.join();t2.join();
    18         System.out.println(i);
    19     }
    20 }
    

    上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在这里,我不厌其烦地再次给出main函数的实现,是希望强调第14、15行代码,也就是Thread的创建方式。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance对象),这样才能保证两个线程在工作时,能够关注到同一个对象锁上去,从而保证线程安全。
    一种错误的同步方式如下

    01 public class AccountingSyncBad implements Runnable{
    02     static int i=0;
    03     public synchronized void increase(){
    04         i++;
    05     }
    06     @Override
    07     public void run() {
    08         for(int j=0;j<10000000;j++){
    09             increase();
    10         }
    11     }
    12     public static void main(String[] args) throws InterruptedException {
    13         Thread t1=new Thread(new AccountingSyncBad());
    14         Thread t2=new Thread(new AccountingSyncBad());
    15         t1.start();t2.start();
    16         t1.join();t2.join();
    17         System.out.println(i);
    18     }
    19 }
    

      上述代码就犯了一个严重的错误。虽然在第3行的increase()方法中,申明这是一个同步方法。但是当锁对象是当前实例,两个线程分别分配了不同实例,导致锁对象不同,不是一把锁,线程安全无法保证。
    但我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。将increase()方法修改如下:

    public static synchronized void increase(){
        i++;
    

      除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被synchronized限制的多个线程是串行执行的)。

    notice:

    什么是管程

    所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。

    Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。

    管程,对应的英文是 Monitor,而不是直译为“监视器”。

    如果对管程这种技术思想有兴趣,可以继续看我的关于这块的笔记。

    相关文章

      网友评论

          本文标题:《实战高并发程序设计》读书笔记-线程基本知识补充

          本文链接:https://www.haomeiwen.com/subject/txdfdltx.html