一、原子性操作的几种方式
先回顾一下原子性操作的解释:原子性要有互斥性,既:同一时刻只能有一个线程进行操作。
1、synchronized 关键字(同步锁),由JVM 管理以及实现
a) 在这个关键字作用对象的对象范围内,多个线程操作是原子性的。(注意:是作用对象的作用范围内)
b) 关键字常见使用方式
b.1 修饰代码块,作用于调用对象,被修饰的代码块同一个对象同一时刻只能有一个线程去访问
b.2 修饰方法,作用于调用对象,被修饰的方法同一个对象同一时刻只能有一个线程去访问
b.3 修饰静态方法,作用于所有对象,被修饰的方法在该类任何实例对象调用时都只能有一个线程访问
b.4 修饰类(参数为class),作用于所有对象,类大括号括起来的,相当于每个方法都加了关键字
c) 示例
@Slf4j
public class SynchronizedTest {
/**
* 未做任何修饰
*/
public void test(int n) {
for (int i = 0; i < 5; i++) {
log.info("test->{}-{}", n, i);
}
}
/**
* 修饰代码块
*/
public void test1(int n) {
log.info("代码块A-{}", n);
synchronized (this) {
for (int i = 0; i < 5; i++) {
log.info("test1->{}-{}", n, i);
}
}
log.info("代码块B-{}", n);
}
/**
* 修饰方法
*/
public synchronized void test2(int n) {
log.info("方法A-{}", n);
for (int i = 0; i < 5; i++) {
log.info("test2->{}-{}", n, i);
}
log.info("方法B-{}", n);
}
/**
* 修饰静态方法
*/
public synchronized static void test3(int n) {
log.info("静态方法A-{}", n);
for (int i = 0; i < 5; i++) {
log.info("test3->{}-{}", n, i);
}
log.info("静态方法B-{}", n);
}
/**
* 锁类
*/
public void test4(int n) {
synchronized (this.getClass()) {
log.info("类A-{}", n);
for (int i = 0; i < 5; i++) {
log.info("test4->{}-{}", n, i);
}
log.info("类B-{}", n);
}
}
public static void main(String[] args) {
/* 每个需要单独执行 */
oneTest();
// twoTest();
// threeTest();
// fourTest();
// allTest1();
// allTest2();
}
// 测试修饰代码块
public static void oneTest() {
// 先输执行非同步块,但是不包含同步块之后的代码,然后执行同步块代码,最后执行同步块之后的代码
final SynchronizedTest st = new SynchronizedTest();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
st.test1(1);
}
});
es.execute(new Runnable() {
@Override
public void run() {
st.test1(2);
}
});
es.shutdown();
}
// 测试修饰方法
public static void twoTest() {
// 在我本机测试结果是,先执行完 21,在执行22
final SynchronizedTest st = new SynchronizedTest();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
st.test2(21);
}
});
es.execute(new Runnable() {
@Override
public void run() {
st.test2(22);
}
});
es.shutdown();
}
// 测试修饰静态方法
public static void threeTest() {
// 在我本机测试结果是,先执行完31,后执行32
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
SynchronizedTest.test3(31);
}
});
es.execute(new Runnable() {
@Override
public void run() {
SynchronizedTest.test3(32);
}
});
es.shutdown();
}
// 测试锁class
public static void fourTest() {
// 在我本机结果是,顺序执行,先执行完 41 ,然后执行 42
final SynchronizedTest st = new SynchronizedTest();
final SynchronizedTest st2 = new SynchronizedTest();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
st.test4(41);
}
});
es.execute(new Runnable() {
@Override
public void run() {
st2.test4(42);
}
});
es.shutdown();
}
// 综合测试,测试静态方法与非静态方法同时调用
public static void allTest1() {
// 在我本机测试结果是,静态方法总是先执行,然后是非静态方法
final SynchronizedTest st = new SynchronizedTest();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
st.test1(51);
}
});
es.execute(new Runnable() {
@Override
public void run() {
SynchronizedTest.test3(52);
}
});
es.shutdown();
}
// 综合测试,测试非加锁方法与加锁方法
public static void allTest2() {
// 在我本机结果是乱序执行
final SynchronizedTest st = new SynchronizedTest();
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
st.test(61);
}
});
es.execute(new Runnable() {
@Override
public void run() {
st.test1(62);
}
});
es.shutdown();
}
}
d) synchronized 关键字不会被子类继承
e) synchronized 不可中断,在线程竞争激烈的时候会降低性能
2、lock 锁,这个是 JDK 提供的代码层面的锁,依赖于CPU特殊指令,典型的类 ReentrantLock (可重入锁,synchronized 也是可重入锁,既同一个线程持有锁后可以再次进行加锁)
a) lock 属于代码级别的锁,它可以被中断,在竞争激烈的时候性能依然保持常态,但是需要注意加锁后必须有相应的解锁操作,否则很容易造成死锁导致系统性能下降或者瘫痪
3、atomic 包性能比JUC中的lock 性能高,但是一次只能处理一个值
二、线程安全可见性
1、解释:某个线程对主内存共享变量的修改能够及时被其它线程观察到
2、导致共享变量的修改没有及时被观察到的原因
a) 线程交叉执行
b) 重排序与交叉执行同时存在
c) 共享变量的值没有在线程工作内存与主内存之间及时更新
3、JVM 对 synchronized 规定
a) 线程解锁前必须将工作内存中的最新值刷新到主内存中
b) 线程加锁时将清空工作内存中共享变量的值,线程需要从主内存中获取最新的值,加锁与解锁必须是同一个把锁
4、volatile 内存可见性
volatile 核心是使用内存屏障(指令)以及禁止指令重排序来保证可见性
a) 对 volatile 写操作时,会在写操作后加入 store 指令将工作内存值刷新到主内存,从而保证可见性,如下解释
普通读 -> 普通写 -> 屏障(store-store) -> volatile 写-> 屏障(store-load) ,第一个屏障防止与前面的写进行排序,第二个屏障用于防止之后可能出现的读写进行排序
c) 对 voltaile 读读操作时,会在读操作前加入 load 指令,将主内存最新值加载到工作内存
volatile 读 -> 屏障(load-load)->屏障(load-store) -> 普通读->普通写 ,第一个屏障防止之后的所有读操作发生指令重排序,第二个屏障是防止之后的写操作不与之前的读操作发生指令重排序。
5、指令重排序规则( happens - before 原则)
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
如果不符合以上规则或者不能从以上规则进行推导出来,则会发生指令重排序。
详细了解需要移步至:【死磕Java并发】-----Java内存模型之happens-before 或者看深入理解JVM
三、对象发布
a) 发布对象:使一个对象能够被当前范围之外的代码使用
示例
/**
* 对象发布
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO1 {
public ReleasePOJO1() {
}
private String[] array = { "1", "2", "3" };
// 不安全
// 这种情况下如果多个线程或者单线程中对返回的数组引用进行了修改,则会修改原始值
public String[] getArray() {
return array;
}
// 安全
// 以副本方式返回数据,及时发生修改,那么改的只是副本
public String[] getArray2() {
if (array == null) {
return array;
}
return Arrays.copyOf(array, array.length);
}
public static void main(String[] args) {
ReleasePOJO1 rp = new ReleasePOJO1();
// 不安全发布
// 原值
log.info("修改前array1-{}", Arrays.toString(rp.getArray()));
// 修改值
rp.getArray()[0] = "A";
log.info("修改后array1-{}", Arrays.toString(rp.getArray()));
// 安全发布
log.info("修改前array2-{}", Arrays.toString(rp.getArray2()));
// 修改值
rp.getArray2()[0] = "B";
log.info("修改后array2-{}", Arrays.toString(rp.getArray2()));
}
}
b) 对象逸出:不安全的对象发布,当一个对象还没有构造完成时就被其它线程使用,这种写法可能会引发未知问题
示例
/**
* 对象逸出演示(错误示例代码,不建议编程时这样写代码)
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO2 {
private String t;
public ReleasePOJO2() {
log.info("POJO2 - 1 - {}", Thread.currentThread().getName());
new Thread(new POJO()).start();
t = "测试逸出";
log.info("POJO2 - 2 - {}", Thread.currentThread().getName());
}
private class POJO implements Runnable {
@Override
public void run() {
log.info("Thread - {}", Thread.currentThread().getName());
// 由于 this 对象 尚未初始化完,所以会先去初始化 this 对象,然后才会执行
log.info(ReleasePOJO2.this.t);
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
es.execute(new Runnable() {
@Override
public void run() {
new ReleasePOJO2();
}
});
}
es.shutdown();
}
}
四、安全对象发布的几种方式
1、在静态初始化函数中初始化对象的引用
2、将对象引用保存在volatile 或 AtomicReference 对象中
3、将对象引用保存在正确构造的某个对象的一个 final 域中
4、将对象引用保存在由锁保护的域中(如示例一)
示例一
/**
* 静态发布示例
*
* @author Aaron
*
*/
public class ReleasePOJO3 {
private volatile static ReleasePOJO3 p3;
private ReleasePOJO3() {
}
public static ReleasePOJO3 getInstance() {
if (p3 == null) {
synchronized (ReleasePOJO3.class) {
if (p3 == null) {
p3 = new ReleasePOJO3();
}
}
}
return p3;
}
}
示例二
/**
* 枚举方式发布
*
* @author Aaron
*
*/
@Slf4j
public class ReleasePOJO5 {
private ReleasePOJO5() {
}
// 实际使用时建议单独放在一个类文件中
public enum Instance {
P5;
// 这里由JVM来保证只会被初始化一次
Instance() {
p5 = new ReleasePOJO5();
}
private ReleasePOJO5 p5;
public ReleasePOJO5 getP5() {
return p5;
}
}
public static void main(String[] args) {
log.info("{}", Instance.P5.hashCode());
log.info("{}", Instance.P5.hashCode());
}
}
五、静态代码块与静态函数执行顺序问题
1、静态代码是按照顺序加载
// 该代码输出结果是null
@Slf4j
public class ReleasePOJO4 {
static {
// 初始化
p4 = new ReleasePOJO4();
}
// 赋值为 null
private static ReleasePOJO4 p4 = null;
public static ReleasePOJO4 getInstance() {
return p4;
}
public static void main(String[] args) {
// 输出是null
log.info("{}", ReleasePOJO4.getInstance());
}
}
// 该代码输出正常
@Slf4j
public class ReleasePOJO4 {
// 赋值为 null
private static ReleasePOJO4 p4 = null;
static {
// 初始化
p4 = new ReleasePOJO4();
}
public static ReleasePOJO4 getInstance() {
return p4;
}
public static void main(String[] args) {
// 输出是 正常的
log.info("{}", ReleasePOJO4.getInstance());
}
}
六、不可变对象
1、创建后就不能修改内部状态的类
2、对象所有域都是final的以及类被声明为final的,如 String 类
3、对象创建期间没有逸出
七、可变对象转不可变对象工具包
1、标准 JAVA 库中的
2、第三方库(Guava,这里以List 为例,它实现了List 接口,并对 addAll 等会对内容进行变更的操作进行了异常抛出)
七、线程封闭
线程封闭基本理解:操作的所有变量、对象状态都在该线程内完成,其它线程也不会影响到该线程。
1、实现线程封闭的几种方式
a) Ad-hoc 方式实现线程封闭,这种方式最不推荐,因为是由程序逻辑控制,这样的实现很糟糕,并且很难维护
b) 堆栈封闭:局部变量,主要是方法内变量,线程执行的时候都操作的是自己的本地内存,因此并发是安全的
c) ThreadLocal 实现线程封闭,主要原理是 ThreadLocal 为每一个线程保存一个变量副本,主要使用map来存储变量值,比较好的线程封闭的实现,使用时需要注意内存泄露问题,在线程结束时调用remove 来清除副本,还有一点,如果传入的是对象或数组,那么在子线程操作该对象或数组时会改变原始值,所以用于读没问题,但是如果多个线程去写存放的对象值,那么这个共享对象本身必须是线程安全的才行,如代码示例。
@Slf4j
public class ThreadlocalTest {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
final ThreadLocal<ThreadlocalTest> tl = new ThreadLocal<>();
// 原始对象
final ThreadlocalTest tt = new ThreadlocalTest();
tt.setName("哈哈哈");
new Thread(new Runnable() {
@Override
public void run() {
tl.set(tt);
tl.get().setName("哈哈哈哈1");
log.info(tt.getName());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
tl.set(tt);
tl.get().setName("哈哈哈哈2");
log.info(tt.getName());
}
}).start();
log.info("main - {}", tt.getName());
}
}