java中保证线程安全的机制
1.概念
1.1进程VS线程
进程:操作系统分配资源的最小单位。
线程:操作系统调度最小单位。
进程与线程关系:
- 有进程必然有线程(一个进程必然有一个线程、一个进程也可有多个线程)。
- 从属关系。
- 线程是轻量级进程(线程启动会比进程稍微轻量级一点)。
1.2线程安全问题
1.2.1内存的共享/私有
共享:堆(方法区/常量池)
私有:栈/PC
原子性、内存的可见性、代码的重排序
- 保证原子性:java的一组指令(天生原子) / 锁
- 内存可见性:JMM(java Memory Model) java内存模型
针对共享数据——线程只能操作工作内存中的数据(读、写)
读:把主内存的数据加载(LOAD)到工作内存;
写:把工作内存的数据写(SAVE)到主内存。 - 代码重排序
概念:CPU / javac编译器 / 运行时的JIT对代码进行的适度优化。
注意:java规定了优化必需保证单线程情况下的正确性(多线程下可能出出错)。
1.2.3引发线程安全问题的因素
- 基础条件:出现共享数据(没有共享数据一定不会存在线程不安全的问题)。
- 共享数据出现写的情况(只读操作不会导致线程不安全)。
- 三个特性(原子性、内存可见性、代码重排序)。
2.synchronized—监视器锁(monitor lock)
2.1语法层面
2.1.1语法
1)作为方法的修饰符(定义方法)
2)作为代码块出现
示例:
public class SynchronizedDemo {
public synchronized void method() {
// 具体代码
}
public synchronized static void staticMethod() {
// 具体代码
}
public void block() {
synchronized (this) { //synchronized (引用)
// 具体代码
}
}
}
2.1.2作用
- java中每个对象都有一个锁 —— 监视锁(monitor lock)。
SynchronizedDemo object = new SynchronizedDemo();
object.method();
- 执行带 synchronized 修饰的普通方法时,首先需要 lock 引用指向的对象中的锁:
1)如果可以锁,正常执行代码;
2)否则,需要等待其他进程把锁释放(unlock)。
解释: 如果一个线程 lock 到了这把锁,到方法执行结束时,就会 unlock 这把锁。
2.2详述
2.2.1锁在什么地方
1)普通方法:锁在调用该方法的引用指向对象中(当前对象即this)。
eg: public synchronized void method() { }
2)静态方法:对当前类对象加锁。
eg: public synchronized static void staticMethod() { }
3)代码块
public void block() {
///
synchronized (this) { //相当于普通锁
// 具体代码
}
synchronized (SynchronizedDemo.class) { //相当于全局锁
// 具体代码
}
}
2.2.2加锁/释放锁
一、原理:
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
二、分类:
A. 普通方法
- 锁的持有和释放——线程状态之间的关系
eg: Runnable(就绪队列)里面有A、B、C三个线程;三者都在抢同一把锁;只有一个CPU。
I. 从三个进程中任意选择一个进程放在CPU上(假设A在CPU上),加锁
II. 执行一段时间后,A被调度出CPU回到就绪队列中(A放弃CPU),此时锁依旧在
III. 假设此时B抢占CPU(锁依旧锁着),抢占资格被剥夺,从就绪队列移到阻塞队列(Runnable状态——》Blocked状态);若C去抢占CPU,结果和B一样
IV. A在CPU中执行完毕时,释放锁(A不一定会释放CPU),同时Blocked状态里的B、C线程重新变成Runable状态
VI. A放弃CPU(A自主退出/时间到了),下一轮的抢锁开始(可能使A抢占成功,也可能是B或C) - 每个锁都有自己的block队列(阻塞队列)
- 即使不是同一方法,但只要是指向同一对象,争抢的就是同一把锁
B.静态方法
eg :public synchronized static void staticMethod() { }
- 类里的锁有时候叫全局锁
2.2.3程序测试
1. 程序测试_普通方法
public class SynchronizedDemo {
public synchronized void method() {
// 具体代码
for (int i = 0; i < 10; i++) {
System.out.println( Thread.currentThread().getName() + ": " + i);
//打印当前线程的名称
if(i==9){
System.out.println("________________");
}
}
}
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
object.method();
}
}
private SynchronizedDemo object;
MyThread(SynchronizedDemo object) {
this.object = object; //同一个对象
//this.object = new SynchronizedDemo(); //不同对象 争抢的是不同的锁 进程不断切换
}
}
public static void main(String[] args) {
SynchronizedDemo object = new SynchronizedDemo();
Thread t = new MyThread(object); //object指向同一个对象
t.start();
while (true) {
object.method();
}
}
}
测试条件一:public void method(){} //方法不加锁
this.object = object; //同一个对象
运行结果一(一部分):
method 方法中 i 不能顺次执行0~9,main 与 Thread-0 不停的抢占 CPU 。
测试条件二:public synchronized void method() //方法加锁
this.object = new SynchronizedDemo(); //不同对象 争抢的是不同的锁 进程不断切换
运行结果二(一部分):
因为不是同一个对象,所以抢的不是同一把锁, 虽然 method 方法加锁,但是 i 还是不能从0~9顺次执行,main 与 Thread-0 不停的抢占 CPU 。
测试条件三:public synchronized void method() //方法加锁
this.object = object; //同一个对象
运行结果三(一部分):
method 方法中 i 顺次执行0~9,必须等一个线程的 method 方法执行完毕,另一个线程才可以抢占CPU(并不意味着一定可以抢占成功,结果可能会出现:main 线程两次或者多次(main: 0 ~ 9)之后,Thread-0 线程抢占CPU成功出现(Thread-0:0 ~9))。
2. 程序测试_静态方法
public class StaticMethod {
public static synchronized void staticMethod() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
StaticMethod.staticMethod(); //同一把锁
}
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true) {
StaticMethod.staticMethod(); //同一把锁
}
}
}
3. 程序测试`_同步互斥
public class SynchronizedTest {
private Object o = new Object();
private static Object T = SynchronizedTest.class;
public static synchronized void t1() throws InterruptedException {
Thread.sleep(999999999); //类对象加锁
}
public synchronized void t2() throws InterruptedException {
//需要根据this对象是否是同一个来判断是否同步互斥
Thread.sleep(999999999); //this对象锁
}
public void t3() throws InterruptedException {
//实例方法调用 成员方法 不一定
synchronized (o){
Thread.sleep(999999999); //this.o对象锁 对象的属性o
}
}
public void t4() throws InterruptedException {
synchronized (T){
Thread.sleep(999999999); //类对象加锁
}
}
public static void main(String[] args) {
//t2、t3方法在不同线程中是否同步互斥? 不一定
//情况一:不同对象,不会同步互斥
new Thread(()->{
try {
new SynchronizedTest().t2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
new SynchronizedTest().t2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//情况二:同一个对象 同步互斥
SynchronizedTest s = new SynchronizedTest();
new Thread(()->{
try {
s.t2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
s.t2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
t1 t2 t3 t4
t1 V X X V
t2 ? X X
t3 ? X
t4 V
2.2.4synchronized代码表现
表现 | 锁的对象 | 什么时期加 | 什么时候释放 |
---|---|---|---|
修饰普通方法 | this | 进入方法 | 正常/异常退出方法 |
修饰静态方法 | 类 | 进入方法 | 正常/异常退出方法 |
修饰代码块 | 小括号内引用指向的对象 | 进入代码块 | 正常/异常退出代码块 |
补充:SynchronizedDemo.class
就是类 SynchronizedDemo 的对象
3.synchronized和原子性/可见性/重排序的关系
3.1原子性
线程之间必须锁的是同一把锁,才可以保证原子性。
3.2可以保证一定限度的可见性
解释:加锁和释放锁会伴随着工作内存的刷新,在这个时机,保证了内存的可见性,但临界区(加锁 —> 释放锁之间的代码)的执行期间不做任何保证
public class SyncVisible {
private static int n = 0;
private static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
synchronized (SyncVisible.class) {
n++;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
for (int i = 0; i < 100000; i++) {
synchronized (SyncVisible.class) {
n--;
}
}
thread.join();
System.out.println(n);
}
}
结果:0
class Demo{
public static synchronized void m1(){}//静态同步方法
public static void m2(){}//静态方法
public synchronized void m3(){}//同步方法
public void m4(){}//普通放法
}
//d1\d2指向同一对象
Demo d1 = new Demo(); Demo d2 = d1;
Demo d3 = new Demo();
//不互斥:允许穿插执行 但不能影响你代码的执行
线程A | 线程B | 是否互斥 | 补充 |
---|---|---|---|
m1 | m1 | 互斥 | 抢同一把锁 |
m1 | m2 | 不互斥 | B线程无与锁有关的东西 |
d1.m3() | d3.m3() | 不互斥 | 对象不同 两把锁 |
d1.m3() | d1.m4() | 不互斥 | B线程无与锁有关的东西 |
3.3解决代码的重排序问题
锁之前的语句无法重排序到临界区(锁里面的代码),临界区内部的无法重排序到外部。
eg:A B C synchronized{D E F} G H
允许{D E F}穿插在其他代码中执行,不允许{D E F}顺序不可更改
3.4synchronized缺点
理论上所有问题都可以用synchronized解决,但成本非常大(线程调度的成本非常大)。
4.volatile(稍轻量级)
4.1volatile—变量修饰符(修饰变量)
语法:用来修饰变量—变量的修饰符(属性/静态属性)
注意:修饰局部变量无意义,因为局部变量不是线程之间共享的。
- 可以保证该变量的可见性问题
- 可以保证部分代码的重排序问题
eg:Object o = new Object();
//可保证1.new() 2.初始化 3.对象到引用的赋值 —— 顺序不变
4.2赋值语句是否原子?
关于原子性:
int a = 100; int n;
boolean b = true; boolean m;
long c = 1ooL; long o;
是原子吗?
字面量 变量
n = 0; 是原子的 n = a; 不是原子的
m = true; 是 m = b; 不是
o = 1000L; 不是 o = c; 不是
//long64位的 Java有可能运行在64/32位机器上 若运行在32位机器上则不是原子的(分为高32位和低32位)
//long、double64位
//float是原子的
基本数据类型变量被赋值 | 基本数据类型变量被赋值 | 是不是 原子的 |
---|---|---|
boolean、byte、short、int、char、float | 字面量 | 原子 |
boolean、byte、short、int、char、float | 变量 | 不是原子 |
long、double | 任何情况 | 不是原子 |
注意
只有可见性问题,没有原子性问题,可以使 volatile 发挥作用。
volatitle long a = 10; //是原子的
5.单例模式
https://editor.csdn.net/md/?articleId=104155700