high-level——线程安全(8)

本文详细介绍了Java中的定时器ClassTimer及其使用,通过示例展示了如何安排任务执行和循环任务。同时,文章深入讨论了线程安全问题,包括线程不安全的场景、Java内存模型、并发编程的关键问题以及解决线程安全的方案,如synchronized关键字。文章还探讨了synchronized的锁机制和不同级别的锁,强调了线程同步在并发编程中的重要性。
摘要由CSDN通过智能技术生成

一、java 定时器

  • Class Timer 是定时器类,它可以按照指定的时间开始执行一份任务;还可以让任务以规定的时间间隔循环执行。

  • 在 java 的应用中,凡是需要按照时间的规律来执行调度的任务,基本上都与 Timer 类有关系,都用到了它。

  • package com.zhong.test_4;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerDemo {
    
        private void init(){
            new Timer().schedule(new MyTask()3000);
        }
    
        public static void main(String[] args) {
            MyTask task = new TimerDemo().new MyTask();
            new Timer().schedule(task,3000);
            new TimerDemo().init();
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("MyTask2 经过3000ms");
                }
            }3000);
        }
    
        class MyTask extends TimerTask{
            @Override
            public void run() {
                System.out.println("MyTask1 经过3000ms");
            }
        }
    }
    

二、Class TimerTask 类

  • 该类实现了Runnable接口,因此该类的对象可以作为线程的执行目标。Timer类主 要用到的方法是schedule方法,该方法有多种重载形式,可以让任务延迟执行,或定点执行,也可以让任务循环执行。代码演示。

  • package com.zhong.test_4;
    
    import java.util.Calendar;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class MuZiDemo {
    
        private static int time = 0;
        private static String str = null;
        public static void main(String[] args) {
            new Timer().schedule(new MyTask()2000);
            while(true){
                System.out.println("计时:" + Calendar.getInstance().get(Calendar.SECOND));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    
        static class MyTask extends TimerTask {
            @Override
            public void run() {
                str = str == "母弹"?"子弹":"母弹";
                time = time == 1000?2000:1000;
                System.out.println(str);
                new Timer().schedule(new MyTask(),time);
            }
        }
    
    }
    

    二、线程安全

    1. 什么是线程安全
      • java是多线程的,对于一个确定的任务,必须保证执行的结果是正确的。如果执行结果不正确(哪怕是百万分之一的错误率都是错误的),这种情况就线程不安全。所以,线程安全就要求程序无论什么时候执行都是正确的。
      • 在内部类中使用外部类的局部变量,需要把变量声明为 final ,这是一个安全性的考虑,编译器担心内部类会改变外部变量的值,可能会对外部类的执行造成不确定的结果。
      • 两个线程调用同一个方法向控制台打印,发生本来应该独立的两部分结果相互穿插,造成执行结果的混乱。原因在于线程的并发执行,当A线程的方法未执行完成时失去了时间片,B线程此时得到了时间片,造成B提前执行,A滞后完成。另外控制台只有一个,它被两个线程共享,也会发生共享冲突(同一时刻两个线程都向控制台打印)。
      • 多个线程针对同一个变量执行加法,可能每次执行的结果都不一样,但结果只会比正确结果小,而不会大。原因在于 count 中的数据是多个线程的共享资源。此时,多个线程也是并发执行。
      • tickCount = 100,该成员变量是共享资源,同时也有多个线程对方法进行并发执行,就会发生了对一个变量多个线程同时进行修改,会发生一张票同时售给多人的情况。无论发生什么情况,售出的总票数是正确的。
    2. 问题总结
      • 以上三个程序都发生了线程不安全的情况,它们都具有共同的特征,第一多线程并发执行,第二使用了共享资源(主要是同时在修改变量,不修改不会有问题)。
    3. java 内存模型(java memory model,JMM)
      1. java 为什么会有内存模型?
        • java 是跨平台的,它用的最多的是 linux 和 windows 两种操作系统,它必须保证程序在不同操作系统上执行要得出正确的结果。不同操作系统的 jvm 是不同的,把同一个字节码解释为不同的机器码。所以 java 内存模型主要用来隔离不同操作系统及硬件之间的差异。所以,遵循内存模型来思考和判断程序执行的情况,就不用再考虑操作系统和硬件的差异,更有利于设计者写出正确的程序。
        • 方法区,在类里面,只有一份,常量池在里面,静态变量在里面,只有一份。所有类的方法也在里面。
        • 堆区,所有的对象创建后都存放在堆区,类中基本类型和引用类型的成员变量都与类在一起,引用类型的成员变量会指向堆中另一个对象。如果方法中用到了对象中的成员变量,该变量就会进入到栈区。
        • 虚拟机栈,每个线程都有自己的线程栈,线程栈有多个栈帧,每个栈帧对应线程调用的一个方法。在方法中使用的基本类型的数据存放于栈中,引用类型的数据存放在堆中。
        • 八个先行发生原则,重点是一个线程内,代码的执行顺序是不变的。虽然有指令重排,但是得到的结果与预期是一致。线程的 start() 方法先于线程的终止。对象的创建先于对象的 finalize() 方法。lock 先于 unlock。
        • 八个原子操作。lock,read,load,user,assign,store,write,unlock。只能保证单个操作是连续的,不能保证所有的操作连续执行。
        • 指令重排序,为了提高程序的执行效率,包含编译器重排, jvm 重排,系统重排。有一个原则,叫指令依赖,两条指令都要使用同一个变量,而且其中有对变量的修改,就不能重排。
    4. 线程安全问题的分析
      • 为了避免多个线程同时操作同-一个变量,要求每个线程从 read 到write 的整个过程是原子的,就是一个线程全部操作完成后,另一个线程才能开始做同样的操作。
      • 因此,解诀线程安全的根本做法,就是要保证多个线程在操作同一资源时,应该进行串行的操作,一个线程从读到写回主内存的整个过程不能被中断,是原子操作,具有排它性。达到此要求,就可以保证线程安全的问题。
    5. 解决线程安全的方案
      • 最主要也是最根本的方案是加锁 lock 和解锁 unlock 。最基本的处理方式就是使用 synchronized 关键,把它用在代码块或方法上。synchronized 修饰的代码块和方法就具有了原子性和排它性,也就是代码块和方法在同一时刻只能有一个线程执行。

    三、并发编程中的关键问题

    1. 变量的不可见性。
    2. 变量的竞争。

    四、线程同步情况下问题的解决

    1. 加锁,synchronized,可重入锁,读写锁,CAS。不同线程干同一件事情。
    2. 实现线程的协调工作。不同线程干不同的事情。

    五、synchronized 关键字

    • 为了 synchronized 的代码块或方法被线程执行时可以起到同步的作用。
    • 同步:线程之间有序的执行,串行执行,它与并行截然相反,效率不高。
    • 异步:线程之间不同步,可能会并发或并行。效率更高。
    • 语法:
      1. 用 synchronized (obj) 包住需要同步的代码,此时这块代码称为同步代码块。一个线程连续的执行完代码块,其他线程才能执行。
      2. synchronized 用来修饰方法,不需要显示的指定对象,此时该方法为同步方法。一个线程执行完方法后,其他线程才能调用。
    • 线程只有得到代码块或方法的锁才能进入执行,否则就等待。在任何时刻只能有一个线程得到锁。这样就保证了线程的同步执行。
    • synchronized 是一个古老的关键字,在 jdk1.5之前,它的锁是一个重量级的锁,就是得到锁的线程才能执行,没得到锁的线程只能阻塞等待(阻塞同步),jdk1.5之后,synchronized 的锁具有了多样性。
    • 现在,它的锁分为四种:
      1. 无锁;未使用 synchronized
      2. 偏向锁;java设计者认为大多数情况下,加了 synchronized 的代码只是被一个线程执行。因此当第一个线程进入 synchronized 代码块时,给的是偏向锁,它只在对象头中记录当前线程的 id ,这个线程下次再执行时,不需要再去申请锁也就是执行 monitorenter 指令,也就不会进行 monitorexit 了,就像未加锁。
      3. 轻重量级锁;如果现在的锁是偏向锁,可是来了第二个代码块也要执行 synchronized 代码块,此时锁会被升级为轻量级锁。它可以保证同一时刻只有一个线程在执行,其他的线程不会阻塞,而是进行自旋(类似于while(true){ }),自旋的线程一旦拿到锁可以马上执行 synchronized 代码块。自旋会消耗CPU的资源,要求 synchronized 代码块执行时间不能太长。此时也没有真正加锁,采用的是 CAS(compare and swap)。所谓CAS操作,就是多个线程可以同时拿到主内存中变量的值,此时有存在三个值(N O V),O表示主内存的旧值,N是新值,表示栈中修改后的值,V表示写回主内存时主内存中当前变量的值,如果O不等于V,表示线程在对变量的处理阶段,已有其他线程修改了该值,N会被丢弃不写回主内存,此时线程会再拿到当前的新值再修改,直到O等于V时,就可以把N写回主内存。但是它存在ABA问题,线程拿到变量以后进行修改,写回去的时候发现O等于V,但不一定是以前的v,为了解决此问题,就给主内存中的变量增加版本号,版本号随着修改的次数而发生改变,以后,不仅要比较O及V,还要比较V的版本号是否一致,只要两者都一致才可以写回去。
      4. 重量级锁。在轻量级锁的情形下,当自旋的线程数量达到一定值或自旋的时间比较长,就会升级为重量级锁。此时要用到监视器对象(偏程),被加锁的对象的对象头中要指向监视器对象,在这种情况下,加锁和解锁的过程比较耗时,造成执行效率的下降。
    • 对象的结构
      • 对象在堆中,由两部分组成,一个是对象头,一个是对象中的实例。
      • 对象头包含三个部分,一个是对象的 hashcode,另一个是 Mark Word,如果是数组还包括数组的长度 length。
    • synchronized 加锁的对象
      • 无论是代码块还是方法,都必须存在一个对象,这个对象会有两种状态,一种是有锁,另一种是无锁,只有该对象在无锁的情况下,其他线程才能得到锁,此时状态变为有锁。线程观察 synchronized 的代码是否有锁,看的就是该对象是否有锁。不同线程能够针对该代码块同步执行,必须保证该对象的唯一性(只能是同一个对象)。
    • synchronized 一般称为同步关键字或互斥关键字,仍然是当前解决线程同步的主要手段,它也一直被改良。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Æ_华韵流风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值