JUC并发编程系列文章
http://t.csdn.cn/UgzQi
文章目录
前言
一、日期转换的问题
问题提出 (线程不安全类SimpleDateFormat)
在多个线程同时调用 SimpleDateFormat 类,对日期进行操作,就会出现线程安全问题
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
@Slf4j(topic = "testThread25")
public class testThread25 {
public static void main(String[] args) {
/**
* 可变的类如果不添加线程安全问题的保护就会出现线程安全问题
* java.lang.NumberFormatException: For input string: "E19514195E41.1951E4E4"
* at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
* at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
解决方案一:加synchronized对象锁
加锁虽然可以,但是会影响性能
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 50; i++) {
new Thread(() -> {
synchronized (sdf) {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
解决方案二:使用不可变的类在多线程环境下进行操作
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!
这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:DateTimeFormatter
不可变对象,实际是另一种避免竞争的方式。
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
@Slf4j(topic = "c.testThread25")
public class testThread25 {
public static void main(String[] args) {
/**
* 使用不可变的类在多线程环境下进行操作,不会有线程安全问题
* "11:48:04 [Thread-9] c.testThread25 - {},ISO resolved to 1951-04-21
* 11:48:04 [Thread-4] c.testThread25 - {},ISO resolved to 1951-04-21
* 11:48:04 [Thread-6] c.testThread25 - {},ISO resolved to 1951-04-21
*/
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//日期转换
TemporalAccessor parse = dtf.parse("1951-04-21");
log.debug("{}",parse);
}).start();
}
}
private static void test(){
/**
* 可变的类如果不添加线程安全问题的保护就会出现线程安全问题
* */
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
}
}
二、不可变的设计
保护性拷贝
但是这种保护性拷贝的次数一但太过频繁,创建对象的个数可能会过多,为了解决这个矛盾,往往会在保护性拷贝的后面紧跟一个 亨元模式
享元模式🔞
1、简介
2、体现
以Long保证类为例,会创建一个LongCache[ ] 缓存数组。缓存 -128 ~ 127 的Long包装类对象,提供外界共同使用,避免了对象的重复创建,但是如果超出了缓存范围就会创建新的Long对象。
3、DIY自定义连接池
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* 自定义连接池
*/
public class testThread26 {
public static void main(String[] args) {
pool pool= new pool(2);
for (int i = 1; i <= 5;i++){
new Thread(()->{
//借出连接
Connection borrow = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//归还连接
pool.still(borrow);
}
}).start();
}
}
}
@Slf4j(topic = "c.pool")
class pool{
//连接池大小
private final int poolSize;
//连接池数组
private Connection[] connections;
//连接池数组状态 为了防止多线程同时修改连接状态需要使用原子数组
private AtomicIntegerArray states;
//初始化连接池
public pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
//创建连接池
for (int i = 0; i < poolSize; i++) {
connections[i] = new MyConnection("连接" + (i+1));
}
}
//借出连接
public Connection borrow(){
//检查连接是否空闲
while (true){
for (int i = 0; i < poolSize; i++){
if(states.get(i) == 0){
//表示连接空闲
//修改连接状态
boolean b = states.compareAndSet(i, 0, 1);
if (b){
log.debug("借出连接"+ (i+1));
return connections[i];
}
}
}
//没有空闲的连接就等待
synchronized (this){
try {
log.debug("等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//归还连接
public void still(Connection connection){
//判断归还的连接是否合法
for (int i = 0; i < connections.length; i++) {
if (connections[i] == connection) {
log.debug("归还连接"+ (i+1));
states.set(i,0);
//归还连接后唤醒正在等待的线程
synchronized (this){
this.notifyAll();
}
break;
}
}
}
}
class MyConnection implements Connection{
private String name;
public MyConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MyConnection{" +
"name='" + name + '\'' +
'}';
}
}
final原理
获取 fifinal 变量的原理
public class TestFinal {
static int A = 10;
static int B = Short.MAX_VALUE+1;
final int a = 20;
final int b = Integer.MAX_VALUE;
final void test1() {
final int c = 30;
new Thread(()->{
System.out.println(c);
}).start();
final int d = 30;
class Task implements Runnable {
@Override
public void run() {
System.out.println(d);
}
}
new Thread(new Task()).start();
}
}
class UseFinal1 {
public void test() {
System.out.println(TestFinal.A);
System.out.println(TestFinal.B);
System.out.println(new TestFinal().a);
System.out.println(new TestFinal().b);
new TestFinal().test1();
}
}
class UseFinal2 {
public void test() {
System.out.println(TestFinal.A);
}
}
需要从字节码层面看这段代码
匿名内部类访问的局部变量为什么必须要用final修饰
参考 https://blog.csdn.net/tianjindong0804/article/details/81710268
匿名内部类之所以可以访问局部变量,是因为在底层将这个局部变量的值传入到了匿名内部类中,
并且以匿名内部类的成员变量的形式存在,这个值的传递过程是通过匿名内部类的构造器完成的。
为什么需要用final修饰局部变量呢?
按照习惯,我依旧先给出问题的答案:用final修饰实际上就是为了保护数据的一致性。
这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。
这里我插一点,final修饰符对变量来说,深层次的理解就是保障变量值的一致性。为什么这么说呢?因为引用类型变量其本质是存入的是一个引用地址,说白了还是一个值(可以理解为内存中的地址值)。用final修饰后,这个这个引用变量的地址值不能改变,所以这个引用变量就无法再指向其它对象了。
回到正题,为什么需要用final保护数据的一致性呢?
因为将数据拷贝完成后,如果不用final修饰,则原先的局部变量可以发生变化。这里到了问题的核心了,如果局部变量发生变化后,匿名内部类是不知道的(因为他只是拷贝了局不变量的值,并不是直接使用的局部变量)。这里举个栗子:原先局部变量指向的是对象A,在创建匿名内部类后,匿名内部类中的成员变量也指向A对象。但过了一段时间局部变量的值指向另外一个B对象,但此时匿名内部类中还是指向原先的A对象。那么程序再接着运行下去,可能就会导致程序运行的结果与预期不同。
绍到这里,关于为什么匿名内部类访问局部变量需要加final修饰符的原理基本讲完了。
那现在我们来谈一谈JDK8对这一问题的新的知识点。在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰。看似是一种编译机制的改变,实际上就是一个语法糖(底层还是帮你加了final)。但通过反编译没有看到底层为我们加上final,但我们无法改变这个局部变量的引用值,如果改变就会编译报错。