一、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); } } }
二、线程安全
- 什么是线程安全
- java是多线程的,对于一个确定的任务,必须保证执行的结果是正确的。如果执行结果不正确(哪怕是百万分之一的错误率都是错误的),这种情况就线程不安全。所以,线程安全就要求程序无论什么时候执行都是正确的。
- 在内部类中使用外部类的局部变量,需要把变量声明为 final ,这是一个安全性的考虑,编译器担心内部类会改变外部变量的值,可能会对外部类的执行造成不确定的结果。
- 两个线程调用同一个方法向控制台打印,发生本来应该独立的两部分结果相互穿插,造成执行结果的混乱。原因在于线程的并发执行,当A线程的方法未执行完成时失去了时间片,B线程此时得到了时间片,造成B提前执行,A滞后完成。另外控制台只有一个,它被两个线程共享,也会发生共享冲突(同一时刻两个线程都向控制台打印)。
- 多个线程针对同一个变量执行加法,可能每次执行的结果都不一样,但结果只会比正确结果小,而不会大。原因在于 count 中的数据是多个线程的共享资源。此时,多个线程也是并发执行。
- tickCount = 100,该成员变量是共享资源,同时也有多个线程对方法进行并发执行,就会发生了对一个变量多个线程同时进行修改,会发生一张票同时售给多人的情况。无论发生什么情况,售出的总票数是正确的。
- 问题总结
- 以上三个程序都发生了线程不安全的情况,它们都具有共同的特征,第一多线程并发执行,第二使用了共享资源(主要是同时在修改变量,不修改不会有问题)。
- java 内存模型(java memory model,JMM)
- java 为什么会有内存模型?
- java 是跨平台的,它用的最多的是 linux 和 windows 两种操作系统,它必须保证程序在不同操作系统上执行要得出正确的结果。不同操作系统的 jvm 是不同的,把同一个字节码解释为不同的机器码。所以 java 内存模型主要用来隔离不同操作系统及硬件之间的差异。所以,遵循内存模型来思考和判断程序执行的情况,就不用再考虑操作系统和硬件的差异,更有利于设计者写出正确的程序。
- 方法区,在类里面,只有一份,常量池在里面,静态变量在里面,只有一份。所有类的方法也在里面。
- 堆区,所有的对象创建后都存放在堆区,类中基本类型和引用类型的成员变量都与类在一起,引用类型的成员变量会指向堆中另一个对象。如果方法中用到了对象中的成员变量,该变量就会进入到栈区。
- 虚拟机栈,每个线程都有自己的线程栈,线程栈有多个栈帧,每个栈帧对应线程调用的一个方法。在方法中使用的基本类型的数据存放于栈中,引用类型的数据存放在堆中。
- 八个先行发生原则,重点是一个线程内,代码的执行顺序是不变的。虽然有指令重排,但是得到的结果与预期是一致。线程的 start() 方法先于线程的终止。对象的创建先于对象的 finalize() 方法。lock 先于 unlock。
- 八个原子操作。lock,read,load,user,assign,store,write,unlock。只能保证单个操作是连续的,不能保证所有的操作连续执行。
- 指令重排序,为了提高程序的执行效率,包含编译器重排, jvm 重排,系统重排。有一个原则,叫指令依赖,两条指令都要使用同一个变量,而且其中有对变量的修改,就不能重排。
- java 为什么会有内存模型?
- 线程安全问题的分析
- 为了避免多个线程同时操作同-一个变量,要求每个线程从 read 到write 的整个过程是原子的,就是一个线程全部操作完成后,另一个线程才能开始做同样的操作。
- 因此,解诀线程安全的根本做法,就是要保证多个线程在操作同一资源时,应该进行串行的操作,一个线程从读到写回主内存的整个过程不能被中断,是原子操作,具有排它性。达到此要求,就可以保证线程安全的问题。
- 解决线程安全的方案
- 最主要也是最根本的方案是加锁 lock 和解锁 unlock 。最基本的处理方式就是使用 synchronized 关键,把它用在代码块或方法上。synchronized 修饰的代码块和方法就具有了原子性和排它性,也就是代码块和方法在同一时刻只能有一个线程执行。
三、并发编程中的关键问题
- 变量的不可见性。
- 变量的竞争。
四、线程同步情况下问题的解决
- 加锁,synchronized,可重入锁,读写锁,CAS。不同线程干同一件事情。
- 实现线程的协调工作。不同线程干不同的事情。
五、synchronized 关键字
- 为了 synchronized 的代码块或方法被线程执行时可以起到同步的作用。
- 同步:线程之间有序的执行,串行执行,它与并行截然相反,效率不高。
- 异步:线程之间不同步,可能会并发或并行。效率更高。
- 语法:
- 用 synchronized (obj) 包住需要同步的代码,此时这块代码称为同步代码块。一个线程连续的执行完代码块,其他线程才能执行。
- synchronized 用来修饰方法,不需要显示的指定对象,此时该方法为同步方法。一个线程执行完方法后,其他线程才能调用。
- 线程只有得到代码块或方法的锁才能进入执行,否则就等待。在任何时刻只能有一个线程得到锁。这样就保证了线程的同步执行。
- synchronized 是一个古老的关键字,在 jdk1.5之前,它的锁是一个重量级的锁,就是得到锁的线程才能执行,没得到锁的线程只能阻塞等待(阻塞同步),jdk1.5之后,synchronized 的锁具有了多样性。
- 现在,它的锁分为四种:
- 无锁;未使用 synchronized
- 偏向锁;java设计者认为大多数情况下,加了 synchronized 的代码只是被一个线程执行。因此当第一个线程进入 synchronized 代码块时,给的是偏向锁,它只在对象头中记录当前线程的 id ,这个线程下次再执行时,不需要再去申请锁也就是执行 monitorenter 指令,也就不会进行 monitorexit 了,就像未加锁。
- 轻重量级锁;如果现在的锁是偏向锁,可是来了第二个代码块也要执行 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的版本号是否一致,只要两者都一致才可以写回去。
- 重量级锁。在轻量级锁的情形下,当自旋的线程数量达到一定值或自旋的时间比较长,就会升级为重量级锁。此时要用到监视器对象(偏程),被加锁的对象的对象头中要指向监视器对象,在这种情况下,加锁和解锁的过程比较耗时,造成执行效率的下降。
- 对象的结构
- 对象在堆中,由两部分组成,一个是对象头,一个是对象中的实例。
- 对象头包含三个部分,一个是对象的 hashcode,另一个是 Mark Word,如果是数组还包括数组的长度 length。
- synchronized 加锁的对象
- 无论是代码块还是方法,都必须存在一个对象,这个对象会有两种状态,一种是有锁,另一种是无锁,只有该对象在无锁的情况下,其他线程才能得到锁,此时状态变为有锁。线程观察 synchronized 的代码是否有锁,看的就是该对象是否有锁。不同线程能够针对该代码块同步执行,必须保证该对象的唯一性(只能是同一个对象)。
- synchronized 一般称为同步关键字或互斥关键字,仍然是当前解决线程同步的主要手段,它也一直被改良。
- 什么是线程安全