java中保证线程安全的机制
1.概念
1.1进程VS线程
进程:操作系统分配资源的最小单位
线程:操作系统调度最小单位
进程与线程关系:
- 有进程必然有线程(一个进程必然有一个线程、一个进程也可有多个线程)
- 从属关系
- 线程是轻量级进程(线程启动会比进程稍微轻量级一点)
1.2线程安全问题
1.2.1内存的共享/私有
共享:堆(方法区/常量池)
私有:栈/PC
1.2.2三个特性
原子性、内存的可见性、代码的重排序
- 保证原子性: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锁在什么地方
- 普通方法:锁在调用该方法的引用指向对象中(当前对象即this)
eg:public synchronized void method() { }
- 静态方法:
eg:public synchronized static void staticMethod() { }
- 代码块
public class SynchronizedDemo {
public void block() {
///
synchronized (this) { //相当于普通锁
// 具体代码
}
synchronized (SynchronizedDemo.class) { //相当于全局锁
// 具体代码
}
}
}
2.2.2加锁/释放锁
1.原理:
- 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
2.分类:
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(); //同一把锁
}
}
}
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);
}
}
结果:
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.单例模式
- 单例模式:程序运行中,类(Person)只会生成一个(Person)对象(实例),大家共用的是同一个实例。
- 场景:配置项 —— 对象
- 实现:1.饿汉模式 2.懒汉模式
5.1饿汉模式
// 饿汉模式的单例
public class SingletonHungry {
// 不允许外边调用构造方法
private SingletonHungry() {}
private static final SingletonHungry instance = new SingletonHungry();
public static SingletonHungry getInstance() {
return instance;
}
}
5.2懒汉模式
5.2.1懒汉模式-单线程版
/**
* 懒汉模式的单例:单线程环境下正确
* 多线程环境下有线程安全问题
* (可见性和原子性的问题)
*/
public class SingletonLazyVersion1 {
private SingletonLazyVersion1() {}
// 多线程下instance可见性不能保证
private static SingletonLazyVersion1 instance = null;
// getInstance 被第一次调用时,意味着有人需要 instance
// 再进行初始化
public static SingletonLazyVersion1 getInstance() {
//多线程情况下原子性不能保证
// 原子开始
if (instance == null) {
instance = new SingletonLazyVersion1();
}
// 原子结束
return instance;
}
}
多线程环境下有线程安全问题:
1.可见性: private static SingletonLazyVersion1 instance = null;
2.原子性:if (instance == null) { instance = new SingletonLazyVersion1(); }
5.2.2懒汉模式-多线程版-性能低
// 线程安全版本的懒汉单例
public class SingletonLazyVersion2 {
private SingletonLazyVersion2() {}
//不用给instance专门加锁 已经保证了instance的可见性了
//因为getInstance方法释放锁时内存都是可见的(清理工作内存的缓存 读到最新的i)
private static SingletonLazyVersion2 instance = null;
//整体用synchronized加锁 保证原子性
//getInstance()方法是static 对其加锁相当于全局锁(大家用的都是同一把锁)
public synchronized static SingletonLazyVersion2 getInstance() {
if (instance == null) {
instance = new SingletonLazyVersion2();
}
return instance;
}
}
缺点:虽保证线程安全,但锁的粒度过大(开始锁住 结束才释放 每次都在竞争锁)
5.2.3懒汉模式-多线程版-二次判断-性能高
public class SingletonLazyVersion3 {
private SingletonLazyVersion3() {}
//注意:instance必需加volatile 才能防止synchronized里面代码的重排序带来的问题
private volatile static SingletonLazyVersion3 instance = null;
private static SingletonLazyVersion3 getInstance() {
if (instance == null) {
//只有在初始化时才需要抢锁 保证只有一个在线程初始化
synchronized (SingletonLazyVersion3.class) {
if (instance == null) { //二次判断法 确保instance没有被初始化
instance = new SingletonLazyVersion3();
}
}
/* 直接写 错误 必须二次判断
//A B C 都为null return
//假设A抢到锁 A去初始化
//A初始完后 B、C接着枪锁 B抢到锁B初始化
//二次初始化发生错误
//注意:开始抢锁到抢到锁有时间间隔 有可能期间其他对像已经初始化过了
synchronized (SingletonLazyVersion3.class) {
instance = new SingletonLazyVersion3();
}
*/
}
return instance; //不为空 return
}
}
/*
//假设A、B两个线程
//A进去发现instance = null 加锁
//二次判断instance = null
//假设执行初始化时发生重排序:new——赋值——初始化 A被new——赋值,然后A被切出去
//B进来发现 instance 已经被初始化 退出去
//但此时 instance 不可用 发生错错误
//注意:synchronized只能保证外面的程序不会重排序到里面 不能保证里面的代码的顺序
*/