javaday14
目录
1 回顾... 1
2 第十四天:高级:线程锁+ThreadLocal1
2.1 生产者和消费者模型... 1
2.2 等待和通知... 1
2.3 同步监视器... 1
2.4 线程辅助工具... 1
2.4.1 线程池 ExecutorService/Executors. 1
2.4.2 Future/FutureTask/Callable. 1
2.5 创建线程集中方式?. 1
2.5.1 ThreadLocal1
2.6 volatile. 1
2.7 线程锁... 1
2.7.1 悲观锁和乐观锁... 1
2.7.2 两种常见的锁... 1
2.7.3 Synchronized. 1
2.7.4 ReentrantReadWriteLock. 1
2.7.5 两种方式的区别... 1
2.7.6 自旋锁... 1
2.8 日期转换类并发异常... 1
2.8.1 日期转换... 1
2.8.2 解决方案1. 1
2.8.3 解决方案2. 1
2.8.4 解决方案3. 1
2.8.5 解决方案4. 1
3 小笔记... 1
1 回顾
l 抽象类
n 半成品,没有完成的类
n 抽象方法的作用:
u 作为通用方法在父类中定义
u 要求子类必须实现
l final
n 常量,不可变
n 方法,不能重写
n 类,不能继承
l static
n 静态属于类,而不属于实例
n 使用场景:
u 共享的数据
u 工具方法
Math.sqrt()
Integer.parseInt()
String.valueOf()
n 静态初始化块
class A {
static {
类加载时,只执行一次
}
}
l 访问控制符
n public, protected, [default], private
n 尽量使用小范围, 便于维护修改
l 接口
n 作用: 结构设计工具,用来解耦合,隔离实现
n 极端的抽象类
n 接口中只能定义:
u 公开的常量
u 公开的抽象方法
u 公开的内部类内部接口
n interface 代替 class
n implements 代替 extends
n 类可以实现多个接口
n 接口之间继承
interface A extends X,Y,Z {
}
2 第十四天:高级:线程锁+ThreadLocal
2.1 生产者和消费者模型
线程间通信模型
l 生产者产生数据,放入一个数据容器
l 消费者从容器来获取数据
l 线程间解耦
package day14;
import java.util.Queue;
import java.util.Random;
public class Producer extends Thread{
private Queue<Character> queue;
public Producer(Queue<Character> queue) {
this.queue = queue;
}
public void run() {
while (true) {
char c = (char) ('a' + new Random().nextInt(26));
synchronized (queue) {
queue.offer(c);
System.out.println("<< "+c);
}
}
}
}
package day14;
import java.util.Queue;
public class Consumer extends Thread {
private Queue<Character> queue;
public Consumer(Queue<Character> queue) {
this.queue = queue;
}
public void run() {
while (true) {
synchronized (queue) {
Character c = queue.poll();
System.out.println(">>>>>>> "+c);
}
}
}
}
package day14;
import java.util.LinkedList;
public class Test1 {
public static void main(String[] args) {
LinkedList<Character> q = new LinkedList<Character>();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
p.start();
c.start();
}
}
2.2 等待和通知
Object 的方法
n wait()
n notify()
n notifyAll()
l 必须在 synchronized 内调用
l 等待通知的对象,必须是加锁的对象
l wait() 外面,总应该是一个循环条件判断
package day14;
import java.util.Queue;
public class Consumer extends Thread {
private Queue<Character> queue;
public Consumer(Queue<Character> queue) {
this.queue = queue;
}
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
}
}
Character c = queue.poll();
System.out.println(">>>>>>> "+c);
}
}
}
}
package day14;
import java.util.Queue;
import java.util.Random;
public class Producer extends Thread{
private Queue<Character> queue;
public Producer(Queue<Character> queue) {
this.queue = queue;
}
public void run() {
while (true) {
char c = (char) ('a' + new Random().nextInt(26));
synchronized (queue) {
queue.offer(c);
System.out.println("<< "+c);
queue.notifyAll();
}
}
}
}
2.3 同步监视器
遇到 synchronized 关键字,会在加锁的对象上,关联一个同步监视器
2.4 线程辅助工具
2.4.1 线程池 ExecutorService/Executors
l ExecutorService
线程池
u execute(Runnable任务对象)
把任务丢到线程池
l Executors
辅助创建线程池的工具类
u newFixedThreadPool(5)
最多5个线程的线程池
u newCachedThreadPool()
足够多的线程,使任务不必等待
u newSingleThreadExecutor()
只有一个线程的线程池
package day14;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService pool;
pool = Executors.newFixedThreadPool(5);
//pool = Executors.newCachedThreadPool();
//pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 1000; i++) {
pool.execute(new R1(i+1));
}
}
static class R1 implements Runnable {
int id;
public R1(int id) {
this.id = id;
}
@Override
public void run() {
String n = Thread.currentThread().getName();
System.out.println(n+" - "+id);
}
}
}
2.4.2 Future/FutureTask/Callable
l Future 父类
u FutureTask 子类
l Callable
可以代替Runnable,提供更丰富的功能
u 有返回值
u 可以使用泛型指定返回值类型
u 可以抛出异常
Future future = pool.submit(Runnable)
继续执行当前线程的功能
future.get(); //阻塞,等待任务结束,返回null
当前线程继续处理
Future<String> future = pool.submit(Callable)
继续执行当前线程的功能
String r = future.get();//阻塞,等待任务结束后,取得它的执行结果
当前线程继续处理
启动一项并行任务,并在未来任务结束后,获得任务的执行结果
package day14;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TestFuture {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
//任务r1放入线程池并行执行
R1 r1 = new R1();
Future<?> future = pool.submit(r1);
pool.shutdown();
System.out.println("继续执行主线程任务");
System.out.println("在这里等待任务r1结束");
future.get();
System.out.println("确认r1任务已结束");
}
static class R1 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
}
}
}
package day14;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TestFuture2 {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
//任务c1放入线程池并行执行
C1 c1 = new C1();
Future<Double> future = pool.submit(c1);
pool.shutdown();
System.out.println("继续执行主线程任务");
System.out.println("在这里等待任务r1结束");
Double r = future.get();
System.out.println("确认r1任务已结束,并得到了执行结果: "+r);
}
static class C1 implements Callable<Double> {
@Override
public Double call() throws Exception{
Thread.sleep(3000);
return Math.random();
}
}
}
package day14;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test5 {
public static void main(String[] args) throws Exception {
ExecutorService pool =
Executors.newFixedThreadPool(5);
Future<Integer>[] a = new Future[5];
for (int i = 0; i < 5; i++) {
a[i] = pool.submit(new C1(2000000*i, 2000000*(i+1)));
}
int sum = 0;
for (Future<Integer> f : a) {
sum += f.get();
}
System.out.println("质数: "+sum);
pool.shutdown(); //销毁线程池
}
static class C1 implements Callable<Integer> {
int from;
int to;
int count;
public C1(int from, int to) {
if (from<=2) {
from = 3;
count = 1;
}
this.from = from;
this.to = to;
}
@Override
public Integer call() throws Exception {
for (int i = from; i < to; i++) {
if (isPrime(i)) {
count++;
}
}
return count;
}
private boolean isPrime(int i) {
double m = Math.sqrt(i)+1;
for (int j = 2; j < m; j++) {
if (i%j == 0) {
return false;
}
}
return true;
}
}
}
2.5 创建线程集中方式?
两种
l 继承Thread
l 实现Runnable
两个工具辅助创建线程,控制线程的执行
l 线程池
l Callable/Future
2.5.1 ThreadLocal
辅助一个线程持有自己的数据,这个数据与其他线程不共享
把数据绑定到线程,线程当做一条流水线,来传递数据
l 多线程并行时,数据是安全的
threadLocal = new ThreadLocal<Double>();
// 在当前线程上绑定数据
threadLocal.set(2.744625);
// 从当前线程获取绑定的数据
threadLocal.get()
// 从当前线程删除数据
threadLocal.remove()
l 存储结构
u 线程中封装一个 Map
u 用ThreadLocal实例作为键
u 对应的值是绑定的数据
package day14;
public class TestThreadLocal {
static ThreadLocal<Double> threadLocal = new ThreadLocal<Double>();
public static void main(String[] args) {
new Thread() {
public void run() {
a();
b();
c();
clearData();
}
}.start();
new Thread() {
public void run() {
c();
a();
b();
clearData();
}
}.start();
}
static void a() {
String n = Thread.currentThread().getName();
Double d = data();
System.out.println(n+" - "+d);
}
static void b() {
String n = Thread.currentThread().getName();
Double d = data();
System.out.println(n+" - "+d);
}
static void c() {
String n = Thread.currentThread().getName();
Double d = data();
System.out.println(n+" - "+d);
}
static Double data() {
Double d = threadLocal.get();
if (d == null) {
d = Math.random();
threadLocal.set(d);
}
return d;
}
static void clearData() {
threadLocal.remove();
}
}
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的解决思路。使用这个工具类可以很简洁地编写出优美的多线程程序。这里注意线程锁是解决并发安全问题,而ThreadLocal是解决线程共享问题,何为共享呢?
例如:我们要使用一个对象,这个对象要在不同线程中传递,怎么保持一份呢?不是新对象呢?就需要使用ThreadLocal来保存和传递这个对象。
2.5.1.1 结构
可以看到ThreadLocal内部由ThreadLocalMap本质就是一个Map结构。其内部保存Entry对象,Entry的key是弱引用(用完就释放),value是强引用(GC回收,如果有引用回收不了)。ThreadLocal被保存在各自的线程Thread中,线程底层由操作系统保证其隔离。相当于共享变量再每个线程中复制了一份。这样共享变量就不会有访问冲突了。其本质是线程私有了。当然不会造成冲突,从而间接的解决了线程安全问题。实际开发,特别框架中广泛用到。
2.5.1.2 副本
数据保存在ThreadLocalMap集合中,这样数据就被每个线程所绑定,操作系统会维护线程的隔离,也就是不能互相访问,从而巧妙的避免线程安全问题,而它所消耗的资源几乎没有,多线程下也不会发生阻塞,性能非常好,框架底层广泛使用。
2.5.1.3 弱引用强引用
使用ThreadLocal最大一个缺点就是其会发生内存泄漏,那什么原因造成它会发生内存泄漏呢?由于ThreadLocalMap的key是弱引用,而Value是强引用。
查看源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
那什么是弱引用?什么是强引用呢?要讨论ThreadLocal内存泄漏问题,先得了解什么是对象的弱引用,什么是对象的强引用?
这里我们不做深层探讨,要讨论它又要牵扯出GC java的垃圾回收机制,又是一大片概念,一旦展开同学们直接就晕倒了,所以这里我们就不展开了,大家记住java中对象有四种引用关系:强引用、弱引用、软引用、虚引用,大概了解概念即可。
强引用:默认的对象都是强引用,如:String s = “tony”; 由于对象被其他对象所调用,GC干不掉。
弱引用:弱引用生命周期很短,不论当前内存是否充足,都只能存活到下一次垃圾收集之前。也就是说GC时就会被干掉。
2.5.1.4 内存泄漏
对象申请后不释放,积累多了就会发生内存泄漏。这种情况实际开发中非常常见,但非常难以监测。我们可以由很多现象的发生推测内存有泄漏的情况。例如:刚开始我们使用一款软件时操作非常流畅,但使用时间长了,软件开始卡顿,重启一下,又飞快如飞。这就是典型的内存泄漏。
ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏?
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, “tony”));
// 其它业务逻辑
} finally {
threadLocal.remove();
}
2.5.1.5 线程锁和ThreadLocal的区别
线程锁实现了线程的同步,线程排队按顺序执行。线程阻塞,前面没有执行完成,后面就只能等待,前面的执行完成,后面才能进行执行。
概括来说,对于多线程资源共享的问题,线程锁同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
2.5.1.6 评价
【了解】ThreadLocal不好理解,但很好使用,几行代码搞定,get/set/remove方法。一般企业中初级程序员也接触不多,更多是中高级去使用他们。所以初学者不用担心,先学会其形式了解其概念即可。
2.6 volatile
l 共享变量的可见性
l 禁止指令重排的优化
cpu 为了提高运算效率,可能根据一定规则,对运算指令重新排序
a = 6; flag = true
flag = true; a = 6;
l 不能包装原子性, 只能靠锁来解决
l 什么时候使用volatile
n volatile - 易变,不稳定
n 多个线程频繁访问,修改变量
package day13;
public class TestVolatile1 {
/*
* volatile 保证数据的可见性
*
* volatile 涉及一些底层cpu指令
* 可以让一个cpu缓存,监视其他cpu的数据变化,
* 发现其他cpu修改了数据,会把高速缓存数据标记成废弃,
* 会重新从内存复制新数据
*/
public static volatile boolean flag = false;//共享的变量
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T2 t2 = new T2();
new Thread(t1, "T1").start();
Thread.sleep(1000);//为了保证t1比t2先启动,sleep一下
new Thread(t2, "T2").start();
}
static class T1 extends Thread {
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName() + " - flag : " + flag);
break;
}
}
}
}
static class T2 extends Thread {
public void run() {
flag = true;
System.out.println(Thread.currentThread().getName() + " - flag : " + flag);
}
}
}
package day13;
public class TestVolatile3{
/*
* 虽然保证可见性,但是不保证运算的"原子性"
*
* 原子性: 一个线程执行期间,其他线程不能执行以下操作
* 把下面多不操作,整体看做是一步操作
* t = t + 1;
* *) 先复制t的值到cpu高速缓存
* *) 执行+1运算
* *) 把运算结果,写会高速缓存的原地址
* *) 在把cpu中数据,复制回内存
*/
public static volatile int t = 0;
public static void main(String[] args){
Thread[] threads = new Thread[10];
for(int i = 0; i < 10; i++){
//每个线程对t进行1000次加1的操作
threads[i] = new Thread(new Runnable(){
@Override
public void run(){
for(int j = 0; j < 1000; j++){
synchronized (threads) {
t = t + 1;
}
}
}
});
threads[i].start();
}
//等待所有累加线程都结束
while(Thread.activeCount() > 1){
System.out.println(Thread.activeCount());
Thread.yield();
}
//打印t的值
System.out.println(t);
}
}
2.7 线程锁
l synchronized
l Lock
n ReentrantLock
n ReentrantReadWriteLock
u ReadLock
u WriteLock
n 方法:
u lock()
u unlock()
l 重入 reentrant
n 调用同一锁定代码,可以再次进入
n synchronized 可重入
n ReentrantLock 可重入
l 读锁和写锁
n 读锁是一种共享锁
u 可以被多个线程同时,同时访问数据
u 可以提高访问数据的并发性能
n 写锁
u 排他锁,只能被一个线程获得
l Lock 底层原理
n 原子操作工具
u AtomicInteger
u AtomicLong
u AtomicReference
u ...
n CAS
u Compare And Swap
u CAS算法,用非阻塞的方式,来获得锁
n 自旋锁
u 占用cpu资源
u 用忙循环,用CAS来获得锁
2.7.1 悲观锁和乐观锁
l 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
l 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2.7.2 两种常见的锁
l Synchronized 互斥锁(悲观锁,有罪假设)
n 采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
l ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)
n ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。
n 读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
2.7.3 Synchronized
使用同步锁实现Synchronized
package javapro.thread;
public class TicketThread extends Thread{
//总票数,多个线程共享这个变量,能修改 ticket–
private int ticket = 10;
//执行业务,重写父类run方法
@Override
public void run() {
//业务处理,卖票:票–
while(true) { //线程非常多,我想尽量给我资源
synchronized (this) { //对象锁
//判断一个条件,出去条件
if(ticket<=0) { //多线程可能ticket=-1
break; //退出死循环
}
//不出现,线程run方法执行太快,不会发生线程冲突
try { //不能抛出异常,抛出就不是重写run方法
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“窗口:” + Thread.currentThread().getName()
+”, 剩余票数:” + ticket-- );
}
}
}
//3个窗口都买这一个票
public static void main(String[] args) {
//目标
Thread target = new TicketThread();
for(int i=0; i<3; i++) {
new Thread(target).start(); //3个线程共同作用一个target
}
}
}
2.7.4 ReentrantReadWriteLock
package javapro.thread;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TicketLock implements Runnable {
private int ticket = 10;
//jdk1.6前,性能差异很大,1.6后synchronized底层实现类似Lock,性能伯仲之间
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while (true) {
try {
lock.writeLock().lock();
if (ticket <= 0) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“线程:” + Thread.currentThread().getName() + “,还剩票数:” + ticket--);
} catch (Exception e) {
// TODO: handle exception
} finally {
lock.writeLock().unlock(); //防止死锁,会自动释放,而synchronized不会释放
}
}
}
public static void main(String[] args) {
Runnable target = new TicketLock ();
int windows = 3; // 窗口数量
for (int I = 1; I < windows + 1; i++) {
new Thread(target, “窗口” + i).start();
}
}
}
2.7.5 两种方式的区别
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。
2.7.6 自旋锁
synchronized
线程无法获得锁时,会通过cpu上下文切换,来切换线程执行,性能损耗较大
Lock
通过自旋锁(spin-lock)的方式,在线程的活跃状态下(不进行cpu上下文切换),不停地尝试获得锁,直到得到锁后继续执行
伪代码:
for(;;) {
尝试获得锁
如果成果获得锁,返回
}
2.8 日期转换类并发异常
2.8.1 日期转换
SimpleDateFormat类是线程非安全的,多线程并发访问时就会造成异常。怎么证明,我们做过多线程例子轻松就可以证明。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
public static Date parse(String s) {
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
测试方法:
package day12.javapro.threadlocal;
public class TestSimpleDateTime implements Runnable{
@Override
public void run() {
System.out.println(DateUtil.parse(“1997-07-01”));
}
public static void main(String[] args) {
Runnable target = new TestSimpleDateTime();
for(int i=0; i<20; i++) {
new Thread(target).start();
}
}
}
执行出错:
Exception in thread “Thread-4” java.lang.NumberFormatException: For input string: “”
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Long.parseLong(Unknown Source)
at java.lang.Long.parseLong(Unknown Source)
at java.text.DigitList.getLong(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at day12.javapro.threadlocal.DateUtils.parse(DateUtils.java:11)
at day12.javapro.threadlocal.TestDateThread.run(TestDateThread.java:10)
at java.lang.Thread.run(Unknown Source)
Tue Jul 01 00:00:00 CST 1997
Tue Jul 01 00:00:00 CST 1997
Tue Jul 01 00:00:00 CST 1997
由上面我们证明了SimpleDateFormat类的确是线程非安全的,多线程并发访问时这个转换还未完成,那个转换就开始了,导致转换参数错误,从而结果错误。
2.8.2 解决方案1
我们分析下,上面什么原因造成的呢?我们访问的是成员变量,成员变量在高并发下就会产生线程安全问题。那我们说过成员变量共享引起的错误最简单的解决方案就是不共享。那我们就改造成员变量为方法内私有变量,这样每次都new创建新的对象。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
public static Date parse(String s) {
SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
问题是解决了,但每次都new创建新的对象,这样内存空间浪费比较大。显然不是最佳的解决方案。
2.8.3 解决方案2
使用同步锁,但程序显然会阻塞,无法并发执行,工作效率低。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
synchronized public static Date parse(String s) {
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
2.8.4 解决方案3
用jdk1.8中的日期格式类DateFormatter,DateTimeFormatter,但这就需要改动现有项目中所有用到这个工具类的代码,工作量较大。而且API写的很烂,不好用,暂时不推荐使用。
2.8.5 解决方案4
用ThreadLocal,一个线程一个SimpleDateFormat对象
工具类:
package day12.javapro.threadlocal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
private static ThreadLocal<DateFormat> threadlocal = new ThreadLocal<DateFormat>() {
protected DateFormat initialValue() {
return new SimpleDateFormat(“yyyy-MM-dd”);
};
};
public static Date parse(String s) throws ParseException {
return threadlocal.get().parse(s);
}
}
测试类:
package day12.javapro.threadlocal;
import java.text.ParseException;
public class TestDate implements Runnable{
@Override
public void run() {
try {
System.out.println( DateUtils.parse(“2018-07-09”) );
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Runnable target = new TestDate();
for(int i=0;i<10;i++) {
new Thread(target).start();
}
}
}
3 小笔记