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

第一章 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源码如下:

Java多线程编程核心技术读书笔记_第1张图片
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("线程被停止了");
    }
}

问题就解决了。

两张图帮助理解:

程序的私有堆栈:

Java多线程编程核心技术读书笔记_第2张图片
image-20190114155332470

读取公共内存:

Java多线程编程核心技术读书笔记_第3张图片
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关键字。

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

Java多线程编程核心技术读书笔记_第4张图片
变量在内存中的工作过程
  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();
        }
    }
}

运行结果:

Java多线程编程核心技术读书笔记_第5张图片
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();
            }
        }
    }
}

运行结果:

Java多线程编程核心技术读书笔记_第6张图片
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();
    }
}

运行结果
Java多线程编程核心技术读书笔记_第7张图片
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();
        }
    }
}

运行结果

Java多线程编程核心技术读书笔记_第8张图片
image-20190121171211813

你可能感兴趣的:(Java多线程编程核心技术读书笔记)