美文网首页
Java多线程编程核心技术读书笔记

Java多线程编程核心技术读书笔记

作者: Sunrise95 | 来源:发表于2019-01-21 17:37 被阅读0次

    第一章 JAVA多线程技能

    实现多线程编程的方式主要有两种。

    • 继承Thread类

    • 实现Runable接口

      工作时的性质相同,主要是Java不能支持多继承。

      继承Thread类后,执行start()方法的顺序不代表线程启动的顺序。

    如何使用实现了MyRunable的类呢?可以看一下Thread.java的构造函数

    以下是一个使用实例:

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("运行中!");
        }
    }
    
    public class Run {
        public static void main(String[] args){
            Runnable runnable = new MyRunnable();
            Thread thread = new Thread(runnable);
            thread.start();
            System.out.println("运行结束");
        }
    }
    

    运行结果:

    image-20190108102448065

    主要是通过创建Thread对象,将实现了run()方法的对象传入Thread。

    线程安全

    非线程安全:主要是指多个线程对同一个对象中的同一个实例变量进行操作时,出现值被更改、值不同步

    可通过synchronized关键字给任意对象或者方法加锁以达到线程安全的目的。

    Thread.currentThread()和this区别

    Thread.currentThread()指的是正在执行操作的线程,this则是指向的线程对象的线程。

    run()start()区别

    run()是将run()方法交给当前线程执行,与主线程是同步执行。

    start()则是另启线程执行方法,与主线程是异步执行。

    停止线程

    1. 使用退出标志使线程正常退出,也就是当run()方法完成后线程终止。
    2. 使用stop()方法强行终止线程,但是不推荐使用,是过期作废的方法,且会终止正在运行中的线程。
    3. 使用interrupt()中断线程。

    interrupt()其实是标志了一个中断状态,通过判断这个状态终止线程;

    这是三个使用例子:

    if(this.interrupted()){
        break;
    }
    if(this.interrupted()){
        return;
    }
    if(this.interrupted()){
        throw new InterruptedException();
    }
    

    interrupted()方法具有检验中断状态并清除中断标志的功能。

    isInterrupted()不是Static,且该方法仅检测中断状态不清除中断标志。

    sleep()方法后,也就是沉睡中被interrupt()会抛出异常且清除中断标志,与之相反的操作也是一样的结果。

    stop()已经被作废,因为如果强制让线程停止可能使清理性工作不能完成,且会对象进行解锁导致数据不一致。

    暂停线程

    通过suspend()暂停线程,resume()方法恢复线程的执行。

    缺点一是独占。如果使用不当,将造成公共的同步对象的独占,使得其他线程无法访问公 共同步对象。当线程获取到锁时,执行了suspend()就将会造成独占,锁将无法被释放。

    有一个特别的坑,printf()方法内部存在同步锁,这点需要注意。

    缺点二是不同步,容易出现因为线程的暂停而导致数据不同步的情况。

    yield方法

    yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

    线程的优先级

    CPU优先执行优先级较高的线程对象中的任务。

    设置优先级可使用setPriorith(),JDK源码如下:

    image-20190108152443448

    JAVA中线程优先级分为1~10这10个等级,JDK中使用了3个常量来预置定义优先级的值,代码如下:

    image-20190108152628336
    线程优先级的继承特性

    JAVA中线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。

    优先级具有规则性

    高优先级的线程总是大部分先执行完,但不代表高优先级的全部先执行完。当线程优先级差距很大时,谁先执行完和代码的调用顺序无关。

    优先级具有随机性

    优先级较高的线程不一定每一次都先执行完。

    守护线程

    JAVA中存在两种线程,一种是用户线程,另一种是守护线程。

    守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程。

    第二章 对象及变量的并发访问

    synchronized同步方法

    方法内的变量为线程安全

    方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私的特性造成的。私有变量非共享,不被多线程修改,也就不存在线程安全问题。

    实例变量非线程安全

    这时候需要添加synchronized关键字。

    多个对象多个锁

    synchronized锁的是对象的代码和方法,而不是一段代码或者方法。

    synchronized方法与锁对象

    当两个线程访问同一个对象的两个方法时:

    1. A线程先持有Object对象的Lock锁,B线程可以以异步的方式调用Object 对象中的非synchronized类型的方法。
    2. A线程先持有Object对象的Lock锁,B线程如果在这是调用Object对象中的synchronized类型的方法则需等待,也就是同步。
    
    脏读

    解决同一个对象的脏读问题可在对象的get()set()都加上synchronized关键字。

    synchronized锁重入

    关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

    个人理解就是得到锁的线程最优先处理,直到完成该线程的任务。

    出现异常,锁自动被释放

    这也是为了防止死锁的发生。

    synchronized不具有继承性

    比如子类调用父类方法,父类方法中的synchronized将会失效。

    synchronized同步语句块

    ​ 顾名思义,可以锁住代码块,使用例子如下:

    synchronized(this){
        需要锁住的代码
    }
    
    synchronized(this)也是锁定当前对象的

    this是用来指向对象监视器的。

    如果锁定代码块时,对象监视器非同一个对象,如synchronized(方法内的私有对象)则相当于不是同一个锁,程序将异步执行。以下是一个例子:

    public class Service {
        private String usernameParam;
        private String passwordParam;
    //    private String anyString = new String(); //如果是对象监视器是这个对象则同步
    
        public void setUsernamePassword(String username,String password){
            try {
                String anyString = new String(); //方法内的私有对象作为对象监视器,程序将异步调用
                synchronized (anyString){
                    System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入同步块 ");
                    usernameParam = username;
                    Thread.sleep(3000);
                    passwordParam = password;
                    System.out.println("线程名称为: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开同步块 ");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    对象监视器
    对象监视器:在Java中,每个对象和Class内部都有一个锁,Class广义上也是一个单例对象,每个对象和Class会和一个监视器关联,注意措辞,锁是存在于对象内部的数据结构,监视器是一个独立的结构但是和对象关联,相同点是对象一定有一个锁也一定关联一个监视器。另外,监视器是操控线程的,他会维持一个代码数据区和线程队列等,保证同一时刻只有一个线程访问代码数据区,监视器就是通过判断对象里锁来完成这个安全访问的功能的。监视器是比锁更高层次的抽象。具体的操作流程是:当代码进入同步区域时,找到对象关联的监视器,然后调用监视器获取锁的方法,监视器会读取对象头里面有关锁的信息作为参数,然后进行获取锁的操作,或是让当前线程得到锁,或是让当前线程等待,当代码退出同步区域时,找到对象关联的监视器,然后调用监视器释放锁的操作,整个流程大致是这个样子。另外,需要明白的是,所有代码都隶属于某个对象,非静态方法好说,静态方法是和Class对象关联的,广义上也是隶属于某个对象的。这样就能理解为什么多线程为什么能够实现同步了,因为多个线程执行同一个监视器管理的一份临界资源,自然就能处理同步的细节了。

    出处:https://blog.csdn.net/tales522/article/details/80853489

    个人理解:将对象监视器视为分配锁的地方,一次只有一个线程可以进入。进入则获取锁,出门则释放锁。

    线程调用同步方法的顺序是随机的

    由于线程调用同步方法的顺序是随机的,将可能造成脏读现象。比如一个List,A和B线程同时操作List Service类对其进行add()如果List为空,添加数据。在synchronized add()没有设置对象监视器的情况下,将有可能发生脏读。

    为了解决这种原因造成的脏读,可以将对象监视器设为实例变量。

    比如在上个例子中将synchronized add()改为

    public class ListService{
        public add(List list,String data){
            try{
                synchronized(list){
                    list.add()
                }
            }
        }
    }
    

    不再同步方法而是改为同步代码块且将对象监视器该为list,就可以解决这个脏读问题。

    对象监视器的三个结论

    x为非this对象。

    1. 当多个线程同时执行`synchronized(x)`同步代码块时呈同步效果。
    2. 当其他线程执行x对象中的synchronized同步方法时呈同步效果。
    3. 当其他线程执行x对象方法里的`synchronized(this)`代码块时也呈现同步效果。
    
    静态同步synchronized方法与synchronized(class)代码块

    关键字synchronized还可以应用在static静态方法上,是对当前的*.JAVA文件对应的Class类进行持锁。synchronized关键字加到非static方法上时给对象上锁。

    synchronized(class)的作用与synchronized static一样都是锁住class类

    数据类型String的常量池特性

    常量池特性:

    String a = "a";
    String b = "a";
    System.out.println(a == b);
    
    输出结果:
        true
    

    当new String对象时,当后面的对象值与前面对象相同时,后面的对象将视为前面的对象,二者都是同一个对象。因此当synchronized(String对象)时,可能会发生例外,使用了同一个对象监视器。所以在大多数的情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如将synchronized(String对象)改为synchronized(Object对象)

    多线程的死锁

    死锁:不同的线程在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。

    可以使用JDK自带JCONSOLE工具来检测是否有死锁的现象。

    锁对象的改变

    锁对象的属性即使改变,以同一个对象为锁的运行结果还是同步的。(String对象比较特别,需要注意)

    volatile关键字

    voliatile的主要作用是使变量在多个线程间可见

    作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值

    解决异步死循环

    先来看一个例子:

    public class RunThread extends Thread {
        private boolean isRunning = true;
        public boolean isRunning(){
            return isRunning;
        }
    
        public void setRunning(boolean running) {
            isRunning = running;
        }
    
        @Override
        public void run() {
            System.out.println("run");
            while (isRunning == true){
            }
            System.out.println("线程被停止了");
        }
    }
    
    public class Run {
        public static void main(String[] args){
            RunThread thread = new RunThread();
            thread.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread.setRunning(false);
            System.out.println("already been set false");
        }
    }
    

    运行结果:

    run
    already been set false
    

    这是IDEA运行后的结果。但是如果使用同样的代码运行在JVM设置为Server服务器的环境中,运行打印输出相同,但是将会进入死循环。这是因为变量isRunning == true存在于公共堆栈及线程的私有堆栈中。在JVM设置为-SERVER模式时为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true。而代码thread.setRunning(false);虽然被执行,更新的却是公共堆栈中的isRunning变量值为false,所以就一直是死循环的状态。

    解决这样的问题就要使用volatile关键字了,强制线程访问isRunning这个变量时,从公共堆栈中取值。

    修改RunThread代码如下:

    public class RunThread extends Thread {
        volatile private boolean isRunning = true;
        public boolean isRunning(){
            return isRunning;
        }
        public void setRunning(boolean running) {
            isRunning = running;
        }
        @Override
        public void run() {
            System.out.println("run");
            while (isRunning == true){
            }
            System.out.println("线程被停止了");
        }
    }
    

    问题就解决了。

    两张图帮助理解:

    程序的私有堆栈:

    image-20190114155332470

    读取公共内存:

    image-20190114160322601

    ​ volatile的缺点时不支持原子性(整个程序中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节)。个人理解:volatile相当于给变量增加了synchronized。

    对比volatile和synchronized
    1. volatile性能比synchronized好,volatile只能修饰于变量,而synchronized可以修饰方法及代码块。
    2. 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
    3. volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性也可以间接保证可见效,因为它会将私有内存和公有内存中的数据做同步。
    4. volatile解决的事变量在多个线程之间的可见性;而synchronized解决的事多个线程之间访问资源的同步性。

    synchronized包含两个特征:互斥性和可见性

    线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

    volatile非原子的特性

    例子:

    public class Mythread extends Thread {
    
        volatile public static int count;
        private static void addConut(){
            for (int i = 0; i < 1000; i++) {
                count++;
            }
            System.out.println("count= " + count);
        }
    
        @Override
        public void run() {
            addConut();
        }
    }
    
    public class Run {
        public static void main(String[] args){
            Mythread[] mythreads = new Mythread[1000];
            for (int i = 0; i < 1000; i++) {
                mythreads[i] = new Mythread();
            }
            for (int i = 0; i < 1000; i++) {
                mythreads[i].start();
            }
        }
    }
    

    运行结果:

    count= 986804
    count= 987804
    count= 988804
    count= 989804
    count= 990804
    count= 991804
    count= 992804
    count= 993804
    count= 994804
    count= 995804
    count= 996804
    count= 997804
    count= 998804
    

    最终结果不是1000000。

    更改Mythread类,使用synchronized代替volatile

    public class Mythread extends Thread {
        public static int count;
        synchronized private static void addConut(){
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count= " + count);
        }
    
        @Override
        public void run() {
            addConut();
        }
    }
    

    运行结果:

    count= 989000
    count= 990000
    count= 991000
    count= 992000
    count= 993000
    count= 994000
    count= 995000
    count= 996000
    count= 997000
    count= 998000
    count= 999000
    count= 1000000
    

    结果正确。

    关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存。但如果修改实例变量中的数据,如i++,这样的操作其实并不是一个原子操作,也就是非线程安全的,容易出现脏数据。解决的办法就是使用synchronized关键字。

    变量在内存中的工作过程:

    变量在内存中的工作过程
    1. read和load阶段:从主工作内存复制变量到当前线程工作内存
    2. use和assign阶段:执行代码,改变共享变量值
    3. store和write阶段:用工作内存数据刷新主内存对应变量的值。

    volatile只能保证1阶段是实时的不出问题。2、3阶段不能保证同步,这也是容易造成脏数据的原因。

    使用原子类进行i++操作

    除了在i++操作时进行synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。

    需要注意的是原子类addAndGet方法是原子的,但方法和方法之间的调用却不是原子的。解决这样的问题必须要用同步。

    第三章 线程间通信

    wait()作用

    wait()作用是使当前执行代码的线程进行等待,将当前线程置入“预执行队列中”,并且在 wait()所在的代码行处停止执行,直到接到通知或中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁。如果在调用wait()时线程没有持有适当的锁,将抛出IllegalMonitorStateException异常,它是RuntimeException的一个子类,因此不需要TRY-CATCH进行捕捉。

    notify()作用

    notify()也要在同步方法或同步块中调用,调用前线程也必须获得该对象的对象级别锁。如果在调用notify()时线程没有持有适当的锁,将抛出IllegalMonitorStateException异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出notify,并使它获取该对象的对象锁。注意:执行notify()后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完。当第一个获得了该对象锁的wait线程运行完毕也后它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有收到该对象通知,还会继续阻塞在wait状态,直到该对象发出notify或notifyAll。

    个人理解:notify使其他线程重新竞争锁,而不是直接获取锁。

    notifyAll()作用

    notifyAll()notify()相同,区别是notify()只唤醒一个线程,notifyAll()唤醒等待该对象锁的全部线程。

    wait()遇到interrupt()

    当线程呈wait()状态时,调用interrupt()会出现InterruptedException异常。

    生产者/消费者模式

    等待/通知模式最经典的案例就是“生产者/消费者”模式,远离都是基于wait/notify。需要注意的是:wait条件的判断最好使用while而不是if,否则在执行POP时容易抛出异常。唤醒最好使用notifyAll()而不是notify()否则在连续唤醒同类线程的情况下将会出现“假死情况”。

    通过管道进行线程间通信

    可以通过管道流(pipeStream)用于在不同线程间直接传送数据,而无需借助类似临时文件之类的东西。

    Java的JDK中提供了4个类:

    1. PipedInputStream和PipedOutputStream
    2. PipedReader和PipedWriter

    1.用来传递字节流,2.用来传递字符流。

    方法join的使用

    join方法的作用是使所属的线程对象X正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程X销毁后再继续执行线程z后面的代码,换种说法就是等待线程对象销毁,常用于主线程等待子线程。

    join(long)可以设置等待时间。

    join和synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。

    join(long)sleep(long)的区别

    方法join(long)的功能在内部是使用wait(long)来实现的,所以join(long)具有释放锁的特点。sleep(long)不具备释放锁的特点。

    join与异常

    在join过程中,如果当前线程对象被中断,则当前线程出现异常。

    join后面的代码提前运行

    类ThreadLocal的使用

    主要解决的是每个线程绑定自己的值,可以将ThreadLocal比喻成全局存放数据的盒子,盒子中可以储存每个线程的私有数据。

    可以通过继承ThreadLocal类,复写initialValue()方法为类设置初始值。初始值也可以具有线程变量的隔离性。

    类InheritableThreadLocal的使用

    使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。

    通过复写childValue()可以继承值并对值进行修改。

    需要注意的一点是:如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。

    第四章 Lock的使用

    ReentrantLock类

    使用方法
    lock();
    doSomething(); //需要同步的代码
    unlock();
    

    使用Condition实现等待/通知

    Object类中的notify()方法相当于Condition类中的signal()方法。

    Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

    公平锁和非公平锁

    公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的。

    默认情况下,ReentrantLock类使用的是非公平锁。

    使用方法:

    Lock lock = new ReentrantLock(isFair) //isFair为true则为公平锁
    

    一些Lock类的常用方法

    getHoldCount()getQueueLength()getWaitQueueLength()的功能

    int getHoldCount():查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。

    int getQueueLength():返回正等待获取此锁定的线程数。

    int getWaitQueueLength():返回执行了同一个condition.await()的线程数。

    hasQueuedThread()hasQueuedThreads()hasWaiters()的功能

    boolean hasQueuedThread(Thread thread):查询指定线程是否在等待获取此锁定

    boolean hasQueuedThreads():查询是否有线程在等待获取此锁定

    boolean hasWaiters(Condition condition):是否有线程正在等待与此锁定有关的condition条件。

    isFair()isHeldByCurrentThread()isLocked()的功能

    boolean isFair():判断是不是公平锁

    isHeldByCurrentThread():当前线程是否保持此锁定

    isLocked():此锁定是否被线程保持

    lockInterruptibly()tryLock()tryLock(long timeout,TimeUnit unit)

    lockInterruptibly():如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常

    tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定

    tryLock(long timeout,TimeUnit unit):如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

    awaitUninterruptibly()的使用

    condition.awaitUninterruptibly()作用使该线程不可被中断

    awaitUnitl()的使用

    condition.awaitUntil(Time time)相当于wait(Time time),可以被提前唤醒。

    使用Condition实现顺序执行

    使用Condition对象可以对线程执行的业务进行排序规划。

    使用ReentrantReadWriteLock类

    类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务。这样虽然保证了实例变量的线程安全性,但效率低下。所以JDK提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率。

    读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

    “读写”、“写读”、“写写”都是互斥的;而“读读”是异步的,非互斥的。

    简单记忆:写操作与任何操作互斥。

    第五章 定时器

    书上定时器这章介绍的是Timer类的使用,但Timer类存在许多问题,如果使用JDK的工具类来实现定时任务,阿里巴巴推荐使用ScheduledExecutorService类。

    定时器类Timer的使用

    JDK中Timer类主要负责计划任务的功能。

    Timer类的主要作用是设置计划任务,但封装任务的类是TimerTask类。

    执行计划任务的代码要放入TimerTask的子类中,因为TimerTask是一个抽象类。

    方法schedule(TimerTask task,Date time)的使用

    schedule()方法,都是按顺序执行。Task队列中同一个Task只能存在一个,否则将会抛出异常!

    该方法的作用是在指定的日期执行一次某一任务。

    这是一个使用例子:

    public class RunSchedule {
        private static Timer timer = new Timer();
        static public class MyTask extends TimerTask {
            @Override
            public void run() {
                System.out.println("运行时间为:" + new Date().toLocaleString());
            }
        }
    
        public static void main(String[] args){
            try {
                MyTask task = new MyTask();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String dateString = "2019-01-21 10:14:10";
                System.out.println("字符串时间为:" + dateString +  "当前时间为: " + new Date().toLocaleString());
                Date dateRef = sdf.parse(dateString);
                timer.schedule(task,dateRef);
            }catch (ParseException e){
                e.printStackTrace();
            }
        }
    }
    

    运行结果:

    image-20190121101654425

    任务虽然执行完,但进程还未销毁。这是因为创建一个Timer就是启动一个新的线程,这个线程并不是守护线程,它一直在运行。

    通过在

    Timer timer = new Timer(True) //设置程序运行后迅速结束当前的进程。
    

    方法schedule(TimerTask task,Date FirstTime,long period)的使用

    该方法的作用是在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。

    period:填的是间隔时间,以毫秒为单位。

    两种情况

    计划时间早于当前时间

    ​ 如果执行任务的时间早于当前时间,则立即执行Task任务。

    多个TimerTask任务及延时

    ​ TimerTask是以队列的方式一个一个被顺序执行,所以执行的时间有可能和预期的时间不一致,因为前面的任务可能消耗的时间较长,则后面的任务运行的时间也会被延迟。

    TimerTask类的cancel()方法

    作用是将自身从任务队列中清除。

    Timer类的cancel()方法

    作用是任务队列中全部任务清空。

    注意事项:

    Timer类中的cancel()方法有时并不一定会停止执行计划任务,而是正常执行。

    下面是一个例子:

    public class TimerCancelTest {
        static int i = 0;
        static public class MyTask extends TimerTask {
            @Override
            public void run() {
                System.out.println("正常执行了:i= " + i +  " 运行时间为:" + new Date().toLocaleString());
            }
        }
    
        public static void main(String[] args){
            while (true){
                try {
                    i++;
                    Timer timer = new Timer();
                    MyTask task = new MyTask();
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String dateString = "2019-01-21 10:14:10";
                    Date dateRef = sdf.parse(dateString);
                    timer.schedule(task,dateRef);
                    timer.cancel();
                }catch (ParseException e){
                    e.printStackTrace();
                }
            }
        }
    }
    

    运行结果:

    image-20190121105820341

    并不是每个任务都被清空了,这是因为Timer类中的cancel()方法并没有争抢到queue锁,所以TimerTask类中的任务继续正常执行。

    方法schedule(TimerTask task,long delay,long period)的使用

    作用是以相对时间执行定时任务。

    同上,可以是使用schedule(TimerTask task,long delay)方法。

    方法scheduleAtFixedRate(TimerTask task,Date firstTime,long period)的使用

    方法schedule()scheduleAtFixedRate()都会按顺序执行,所以不要考虑非线程安全的情况。

    方法schedule()scheduleAtFixedRate()主要的区别只在于不延时的情况。

    schedule():如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“开始”时的时间来计算。

    scheduleAtFixedRate():如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“结束”时的时间来计算。

    schedule方法不具有追赶执行性

    错过的Task循环任务,就当无事发生,不执行了,这就是Task任务不追赶的情况。

    scheduleAtFixedRate方法具有追赶执行性

    错过的Task循环任务将被“补充性”执行也就是直接运行错过任务的次数。

    第六章 单例模式与多线程

    立即加载/“饿汉模式”

    立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法是直接new实例化。而立即加载从中文的语境来看,有“着急”、“急迫”的含义,所以也称为“饿汉模式”。

    立即加载/“饿汉模式”是在调用方法前,实例以及被创建了。来看一下实现代码。

    public class MyObject {
        private static MyObject myObject = new MyObject();
        private MyObject(){
            
        }
        public static MyObject getInstance(){
            //此版本为立即加载
            //缺点是不能有其他实例变量
            //因为getInstance()方法没有同步
            //所以有可能出现非线程安全问题
            return myObject;
        }
    }
    

    创建线程类如下

    public class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
    

    创建运行类Run代码如下

    public class Run {
        public static void main(String[] args){
            MyThread t1 = new MyThread();
            MyThread t3 = new MyThread();
            MyThread t2 = new MyThread();
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    运行结果 image-20190121140610292

    实现了立即加载型单例设计模式。

    延迟加载/“懒汉模式”

    延迟加载就是在调用get()方法时实例才被创建,常见的实现办法就是在get()方法中进行new实例化。而延迟加载从中文语境来看,是“缓慢”、“不急迫”的含义,所以也被称为“懒汉模式”。

    一个简单实现代码如下

    public class MyDelayObject {
        private static MyDelayObject myObject;
        private MyDelayObject(){
        }
        public static MyDelayObject getInstance(){
            if (myObject == null){
                myObject = new MyDelayObject();
            }
            return myObject;
        }
    }
    

    单线程虽然完成了单例,但如果在多线程的环境中,就会出现取出多个实例的情况。

    缺点

    多线程情况容易创建多个对象。

    public class MyDelayObject {
        private static MyDelayObject myObject = new MyDelayObject();
        private MyDelayObject(){
        }
        public static MyDelayObject getInstance(){
            if (myObject == null){
                //模拟在创建对象之前做一些准备行的工作
                Thread.sleep(3000);
                myObject = new MyDelayObject();
            }
            return myObject;
        }
    }
    

    运行结果

    image-20190121142623730

    返回了不同的对象。

    如何解决呢?

    1.声明synchronized

    get()添加synchronized关键字。

    public class MyDelayObject {
        private static MyDelayObject myObject;
        private MyDelayObject(){
        }
        synchronized public static MyDelayObject getInstance(){
            if (myObject == null){
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                myObject = new MyDelayObject();
            }
            return myObject;
        }
    }
    

    运行结果

    [图片上传失败...(image-65776b-1548063380608)]

    问题解决了,但此种方法效率非常低下,是同步运行的。

    2.尝试同步代码块

    public class MyDelayObject {
        private static MyDelayObject myObject;
        private MyDelayObject(){
        }
        public static MyDelayObject getInstance(){
           synchronized (MyDelayObject.class){
               if (myObject == null){
                   try {
                       Thread.sleep(3000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   myObject = new MyDelayObject();
               }
           }
            return myObject;
        }
    }
    

    同步代码块的效果与声明synchronized关键字相同,问题可以解决,但效率低下。

    3.使用DCL双检查锁机制

    public class MyDelayObject {
        private static MyDelayObject myObject;
        private MyDelayObject(){
        }
        public static MyDelayObject getInstance(){
         try {
             //第一次检查
             if (myObject == null){
                 Thread.sleep(3000);
                 //同步部分代码块
                 synchronized (MyDelayObject.class){
                     //第二次检查
                     if (myObject == null){
                         myObject = new MyDelayObject();
                     }
                 }
         }
         }catch (InterruptedException e){
             e.printStackTrace();
         }
         return myObject;
        }
    }
    

    使用双重检查锁功能,成功的解决了“懒汉模式“遇到的多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。

    使用静态内置类实现单例模式

    使用了这么一个特性:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

    public class MyInnerObject {
        private static class MyObjectHandler{
            private static MyInnerObject myInnerObject = new MyInnerObject();
        }
        private MyInnerObject(){}
        public static MyInnerObject getInstance(){
            return MyObjectHandler.myInnerObject;
        }
    }
    

    序列化与反序列化的单例模式实现

    静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的还是多例。

    需要使用一个readResolve()方法

    使用static代码块实现单例模式

    静态代码块中的代码在使用类的时候就已经执行了,所以可以应用这个特性来实现单例模式。

    public class MyObject {
        private static MyObject myObject;
        
        static {
            myObject = new MyObject();
        }
        private MyObject(){
    
        }
        public static MyObject getInstance(){
            return myObject;
        }
    }
    

    使用enum枚举数据类型实现单例模式

    枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用。

    第七章 拾遗增补

    SimpleDateFormat非线程安全

    SimpleDateFormat类主要负责日期的转换与格式化,但在多线程的环境中,使用此类容易造成数据转换及处理的不准确,因为SimpleDateFormat并不是线程安全的。

    以下是一个例子

    public class MyThread extends Thread {
        private SimpleDateFormat sdf;
        private String dateString;
    
        public MyThread(SimpleDateFormat sdf, String dateString) {
            this.sdf = sdf;
            this.dateString = dateString;
        }
    
        @Override
        public void run() {
            try {
                Date dateRef = sdf.parse(dateString);
                String newDateString = sdf.format(dateRef).toString();
                if (!newDateString.equals(dateString)){
                    System.out.println("报错了 日期字符串: " + dateString + " 转换后的日期为: " + newDateString);
                }
            }catch (ParseException e){
                e.printStackTrace();
            }
        }
    }
    
    public class Run {
        public static void main(String[] args){
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            String[] dateStringArray = new String[]{"2000-01-01","2000-01-02","2000-01-03","2000-01-04","2000-01-05","2000-01-06"};
            MyThread[] threads = new MyThread[6];
            for (int i = 0; i < 6; i++) {
                System.out.println(dateStringArray[i]);
                threads[i] = new MyThread(sdf,dateStringArray[i]);
            }
            for (int i = 0; i <6; i++) {
                threads[i].start();
            }
        }
    }
    

    运行结果

    image-20190121171211813

    相关文章

      网友评论

          本文标题:Java多线程编程核心技术读书笔记

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