非线程安全产生原因:
多个线程访问同一个对象中的实例变量时,会产生脏读,也就是说可能会出现一种情况:取到的数据已经被更改掉。
而线程安全就是保证取到的数据不是脏数据。
所以我们才要仔细去分析,怎样才能保证线程安全,也就是怎样在代码中做一些特殊的处理,从而保证线程安全。、
最基础的保证线程安全的方式是加 Synchronized关键字,此关键字可以加到方法上或者对象上,但是需要注意的是,它取得的锁都是对象锁,而不是将一段代码或者方法当做锁。所以,哪个线程先执行带Synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,如果多个线程访问多个对象,则会创建多个锁,这些锁运行的先后顺序是异步的。
关于 synchronized的锁重入功能:
在使用synchronized时,当一个线程得到一个锁对象后,再次请求此对象锁时是可以再次得到该对象锁。
换种说法就是,在一个synchronized方法、块内部调用本类的其他Synchronized方法、块时,是永远可以得到锁的。
即自己可以再次获取自己的内部锁。
当一个线程执行的代码出现异常时,所持有的锁会自动释放。
用关键字Synchronized同步一个方法是有弊端的,比如A线程调用同步方法执行一个长时间的任务,则B线程需要等待比较长时间,这种情况下可以使用同步代码块实现。
脏读是通过 synchronized 关键字解决的。
当A线程调用了 anyObject 对象中带有关键字(synchronized)的X方法时,A线程就获得了X方法的锁,更确切的讲,是获得了该对象的锁,其他线程必须等待A线程执行完之后才能调用X方法。
分两种情况,第一种情况是B线程要调用 anyObject 对象的非 synchronized 方法,直接调用即可,不需要等待A线程执行完毕。
第二种情况是B线程要调用 anyObject 对象的带有 synchronized 关键字的方法,则需要等待 A 线程执行完之后,等到A线程释放锁之后,B才能接着执行。
注意,使用 synchronized 对方法加锁的时候,虽然可以实现线程安全,但是效率低下,改进方法是使用 synchronized 对代码块加锁,实现的效果同样是对 anyObject 对象加锁,且可以提升效率。使用同步代码块加锁主要有两种情况。
第一种情况是锁 this 对象,即 synchronized(this),这是最常用的加锁方式。
第二种情况是对非this对象加锁,即对除 this 之外的其他任意代码块加锁,好处在于,如果一个类中有很多 synchronized 方法,这时虽然能实现同步,但是会受到阻塞,依然会影响效率,但是如果使用同步代码块锁非 this 对象,锁的非 this 代码块和锁的 this 代码块是异步的,两者不互相抢锁,所以效率会大大提升.
注意,不管是同步方法还是同步代码块,都是对对象加锁。
volatile 关键字:作用是强制从公有内存中读取变量的值,从而实现了变量在多个线程之间的可见性。
volatile和 synchronized 之间的区别和联系:
关键字 volatile 是线程的轻量级实现,所以 volatile 的性能比 synchronized 要好,并且 volatile 只能修饰变量,而 synchronized 用来修饰方法和代码块,随着 JDK 的不断优化, synchronized 效率不断提升,在开发中使用 synchronized 的频率较高。多线程访问 volatile 不会发生阻塞,而 synchronized 会发生阻塞,因为 volatile 是用来修饰变量的。
volatile 能保证数据的可见性,但是不能保证原子性,而 synchronized 可以保证可见性和原子性,因为会间接的将私有内存和公有内存中的数据进行同步。
注意:一个对象只有一把锁,将 static 和 synchronized 结合起来使用的时候,加锁的是该类,即类的所有对象都被加锁,所有对象都会被阻塞,只用 synchronized,加锁的是某一个对象。一个锁对象有两个队列,就绪队列和阻塞队列。
wait()方法释放锁,sleep()方法不释放锁,notify()方法不释放锁。对于notify()方法来讲,必须执行完该方法所在的同步代码块后才会释放锁。
综上, synchronized 是为了解决线程安全性使用的,完全可以替代 volatile的功能。所以 synchronized 主要有3种应用方式:
(1) 修饰实例方法
(2) 修饰静态方法
(3) 修饰 this 代码块
(4) 修饰非 this 代码块
关于线程间通讯,需要掌握以下知识点:
(1) 用 wait/notify 实现线程通讯
(2)生产者,消费者模式的实现
(3)方法 join的使用
(4)ThreadLocal 类的使用
在调用 wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或者同步块中调用 wait()方法,在执行wait()方法后,当前线程释放锁。如果在调用 wait() 方法时没有持有适当的锁,会排出 IllegalMonitorStateException.
在调用notify()或者notifyAll() 也要在同步方法或者同步方法块中调用,在调用之前,也要获得对象级别的锁,否则会抛出 IllegalMonitorStateException异常。
在执行 notify() 方法后,当前线程不会马上释放对象锁,要等到执行 Notify() 方法的线程将程序执行完,即退出 synchronized 代码块后,当前线程才会释放锁。方法 wait()被执行后,锁自动释放。
当线程呈现 wait() 状态时,调用线程对象的 interrupt()方法会出现 InterruptedException 异常,出异常的同时,锁会被释放.
wait() 和 notify()最经典的案例就是生产者/消费者模式。
消费者模式分为很多种情况:
(1) 一生产,一消费,直接用
(2)多生产,多消费,会出现假死的情况,即所有线程都处在 waiting 状态下,解决假死的方法是,用 notifyAll() ,原理是将同类和异类线程同时通知到。
(3) 一生产,一消费, 采用操作栈的方式实现,维护一个容量为1的栈,生产者向栈中写数据,消费者从栈中读数据。
(4)一生产,多消费,采用操作栈的方式实现,维护一个容量为1的栈,一个生产者负责向栈中写数据,多个消费者抢占式的从栈中读数据,解决假死的情况。
(5)多生产,一消费,采用操作栈的方式实现,维护一个容量为1的栈,多个生产者向栈中写数据,一个消费者消费数据。
(6)多生产,多消费,采用操作栈的方式实现,维护一个容量为1的栈,多个生产者向栈中写数据,多个消费者消费数据。
Java中管道流是一种特殊的流,用于在不同线程间直接传递数据,一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。通过使用管道,实现不同线程间的通信,而无需借助临时文件之类的东西。
通过管道进行消费者通信:字节流
Java的JDK中,提供了4个类来处理管道字节流: PipedInputStream,PipedOutputStream, PipedReader, PipedWriter.
通过管道进行消费者通信:字符流(Reader, Writer)
字节流和字符流的区别:
(1) 使用时操作代码不同,字节流(InputStream, OutputStream), 字符流(Reader, Writer).
(2) 字节流不会用到缓冲区,直接操作文件,字符流使用了缓冲区,通过缓冲区再操作文件。
(3) 所有文件在硬盘或者传输的过程中都是以字节的方式进行的,而字符是只有在内存中才会形成,缓冲区是一段特殊的内存,所以字符流使用了缓冲区,在开发过程中,字节流使用的较为广泛。
join()方法:
当主线程需要用到子线程返回值的时候,需要用到join()方法,join()的作用是等待子线程对象的销毁。在主线程中启动子线程之后,立马将当前主线程join()掉,具体底层实现是通过 wait() 进行等待,从而实现该功能的。
join()和 Interrupt()方法彼此遇到,会出现 InterruptedException 异常。
join()和sleep()的区别:效果上区别不大,主要区别在于内部实现方式不同, join()是通过 wait()方法实现的,具有释放锁的特点,而sleep()不释放锁。
当一个对象的变量被多个线程访问的时候,会有线程安全问题,当然我们可以使用 synchronized 关键字加锁进行同步处理,从而限制只有一个线程来使用该变量,但是加锁会影响程序执行效率,所以引入了 ThreadLocal 的概念。
使用 ThreadLocal 维护变量的时候,会为每一个线程提供一个独立的变量副本,每个线程内部都会有一个从内存拷贝过来的变量副本,这样就不会存在线程安全问题,也不会影响程序的执行性能,但是这样处理对资源的消耗会变大,比如内存的使用会变大。ThreadLocal是线程的局部变量,通常是类中的 private static 字段。
Lock是 synchronized 的进阶,在Java并发包中大量的类使用了 Lock接口作为同步的处理方式.
锁 Lock 分为公平锁和非公平锁,公平锁指的是线程获取锁的顺序是按照线程加锁的顺序来分配的,即先入先出顺序。
非公平锁指的是获取锁的抢占机制,是随机获得锁的。
两种锁的使用方式只是 ReentrantLock()中传参方式不同而已,是该方法自己底层实现的,开发者只需要传入 true/false 即可,不需要操控底层逻辑。
关于 Lock()的使用:
(1)ReentrantLock 类 + Condition(await()和signal() 方法) 的使用(可以实现等待、通知模式)
可以创建多个 condition实例(对象监控器),线程注册在指定的 Condition 中,可以实现有选择的进行通知。
使用过程:
lock.lock()
Condition.await()
lock.unlock()
即condition.await()方法调用之前,必须先用 lock获得锁,获得同步监视器。
(2)ReentrantReadWriteLock类的使用
由于ReentrantLock效率低下,所以出现了 ReentrantReadWriteLock,即读写锁
读写锁指的是有两个锁,一个是读操作相关的锁,也称为共享锁,另一个是写操作相关的锁,也称为排他锁。
定时器Timer的使用:内部是使用多线程的方式进行处理的
我们需要注意两方面。
(1) 如何实现指定时间执行任务
(2)如何实现指定周期执行任务
具体做法:
(1) 写一个 TimerTask的子类,继承 TimerTask 类,并重写 TimerTask类的 run()方法。因为 TimerTask类是一个抽象类,具体的执行计划任务的代码要写在 TimerTask 类中。
(2)new 一个 Timer类,在该类的 schedule 方法中调用(1)中执行计划任务的代码。
Timer 类主要负责计划任务的功能,也就是在指定时间开始执行某一个任务,但是封装主要任务的却是 TimerTask类,底层实现是一个线程。
Timer的优点在于简单易用,但是由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一个时间只有一个任务在执行,前一个任务的延迟或者异常都会影响到之后的任务。鉴于上述缺陷,JAVA 5 推出了基于线程池设计的 SchedukedThreadPoolExecutor,设计思想是,任务执行时并发,相互之间不受干扰,为了提高效率。
注意:创建一个 Timer 就是启动一个线程,这个新线程并不是守护线程,所以哪怕定时任务执行完了,该线程还未销毁,一直在运行,解决方法是将Timer在new 的时候变成守护线程。
Task是按照队列的形式,一个一个顺序执行的。
TimerTask类的cancel()方法作用是将自身任务从任务队列中清除,Timer类的cancel()方法作用是将所有队列从队列表中清除,并且进程被销毁。
schedule有6种重载方法:
(1) schedule(TimerTask task, Date time) 作用是在指定的日期执行一次某任务
(2)schedule(TimerTask task, Date firstTime, long period)作用是按照指定的间隔周期无限循环的执行某一任务
(3)schedule(TimerTask task, long delay)作用是以执行本方法当前的时间为参考时间,在此基础上延迟一定的时间,再执行TimerTask任务。
(4)schedule(TimerTask task, long delay, long period)作用同(3),不同点是会多次执行该任务。
(5)scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
(6)scheduleAtFixedRate(TimerTask task, long delay, long period)
后两个暂时还没搞太明白,待更。
关于懒汉模式和饿汉模式:
1. 立即加载,饿汉模式指的是在调用方法前,就已经创建好了一个实例,所以不会存在线程安全问题。
2. 延迟加载,懒汉模式指的是在调用 get()方法时实例才会被创建,常见的实现方法就是在get()方法中 new 实例化,如果只在单线程环境下,不会出现问题,但是如果是在多线程环境下,就会出现线程安全问题。
3. 延迟加载,懒汉模式避免线程安全的方式:
(1) 用 synchronized 给方法加锁,给整个方法加锁,效率低,不提倡使用,
(2) 用 synchronized 给代码块加锁,全部代码被上锁,执行效率依然低
(3) 用 DCL双检查锁机制,在实践中不太好用,双重锁指的是在同步代码块之前检查一遍,在同步代码块内部再检查一遍
(4) 用静态内置类实现, private static class A{ },因为静态内部类只能访问静态成员,不能访问实例成员
(5) 用static 代码块实现单例模式,静态代码块中的代码在使用类的时候就已经执行了
(6) 用enum枚举,在使用枚举的时候,构造方法会被自动调用,所以在枚举类的构造方法中直接 new 就行。