一个对象是否需要是线程安全的,取决于它是否被多个线程访问。
要使得对象是线程安全的,需要采用同步机制来协同对对象的可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。
1、抢占式执行
java中线程调度采用抢占式调度方法。许多线程可能是可运行状态但只能有一个线程在运行该线程将持续运行直到它自行终止或者是由于其他的事件导致阻塞亦或者是出现高优先级线程成为可运行的则该线程失去CPU的占用权。而非其他编程语言采用有轮回式的方式。
2、多个线程修改同一个变量
3、非原子性操作
对于比如 count++ 操作,它并不是一个原子性操作,它的操作是分为三步的:1. 查询 count 当前的值【load】 2.进行 count+1 操作【++】 3. 刷新 count 的最新值【save】。
当在两个线程中同时执行时,
想要得到的结果是0但是最终的结果却不是我们想要的也不是我们可以控制的。
4、内存可见性问题
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
线程之间的共享变量存在主内存 (Main Memory).
每一个线程都有自己的 “工作内存” (Working Memory) .
当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 当线程要修改一个共享变量的时候,
也会先修改工作内存中的副本, 再同步回主内存。
如下场景:
① 初始情况下, 两个线程的工作内存内容一致。
② 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的 工作内存的 a 的值也不一定能及时同步。
这就是内存的不可见性~
5、指令重排序
指令重排序是编译器优化代码的一种操作,我们大部分写的代码中,彼此的顺序(不会影响程序逻辑),谁在前谁在后无所谓~编译器会通过调整代码的前后顺序从而提高程序效率。 那么如果是一个多线程的程序,编译器在优化代码时出现了误判,优化了不应该更改的顺序。就也会导致线程安全问题。
分类 | 具体分类 | 被锁的对象 | 伪代码 |
方法 | 实例方法 | 类的实例对象 | //实例方法 public synchronized void method(){ ... } |
静态方法 | 类对象 | //静态方法,锁住的是类对象 public static synchronized void method(){ ... } |
|
代码块 | 实例对象 | 类的实例对象 | //同步代码块,锁住的是该类的实例对象 synchronized(this){ ... } |
class对象 | 类对象 | //同步代码块,锁住的是该类的实例对象 synchronized(XXX.class){ ... } |
① 互斥(排他性)
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也 执行到同一个对象 synchronized 就会阻塞等待.
② 刷新内存(内存不可见问题)
synchronized 的工作过程:
获得互斥锁
从主内存拷⻉变量的最新副本到⼯作的内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
③ 可重入
synchronized 同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题。
/**
* synchronized 可重入性测试
*/
public class ThreadSynchronized {
public static void main(String[] args) {
synchronized (ThreadSynchronized.class) {
System.out.println("当前主线程已经得到了锁");
synchronized (ThreadSynchronized.class) { //可以进入第二层
System.out.println("当前主线程再次得到了锁");
}
}
}
}
会打印2次:当前主线程再次得到了锁
synchronized 同步锁是通过 JVM 内置的 Monitor 监视器实现的,而监视器又是依赖操作系统的互斥锁 Mutex 实现的。JVM 监视器的执行流程是:线程先通过自旋 CAS 的方式尝试获取锁,如果获取失败就 进入 EntrySet 集合,如果获取成功就拥有该锁。当调用 wait() 方法时,线程释放锁并进入 WaitSet 集合,等其他线程调用 notify 或 notifyAll 方法时再尝试获取锁。锁使用完之后就会通知 EntrySet 集合中的线程,让它们尝试获取锁。
- synchronized 执行流程
在 Java 中,synchronized 是非公平锁,也是可以重入锁。
- 所谓的非公平锁是指,线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取到锁。
- 可重入锁指的是,一个线程获取到锁之后,可以重复得到该锁。也就是多层的获取进入这个锁
Lock 是一个接口,一般使用 ReentrantLock 类作为锁。在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。
public class MyReentrantLock implements Runnable
{
private Lock numLock=new ReentrantLock();
@Override
public void run() {
for (int i=1;i<10;i++)
{
//加锁
numLock.lock();
System.out.println(Thread.currentThread().getName()+":还有"+i+"人");
//释放锁
numLock.unlock();
}
}
}
public class demo {
public static void main(String[] args) {
MyReenrantLock myTread=new MyReenrantLock();
Thread T1= new Thread(myTread,"线程1");
Thread T2= new Thread(myTread,"线程2");
Thread T3=new Thread(myTread,"线程3");
T1.start();
T2.start();
T3.start();
}
}
lock.lock();
try {
}finally {
lock.unlock();
}
注意1 :把 unlock() 放在 finally 块中以防死锁
注意2 :把 lock() 放在 try 外或者try的首行,因为1. try 代码中的异常导致加锁失败,还会执行 finally 释放锁操作。2. 释放锁的错误信息会覆盖业务代码报错信息,从而增加调试程序和修复程序的复杂度。
Lock 公平锁和非公平锁
构造方法 :
ReentrantLock() : 创建一个 ReentrantLock的实例。
ReentrantLock(boolean fair) : 根据给定的公平政策创建一个 ReentrantLock的实例。
默认创建一个非公平锁(线程竞争,非公平锁性能更高),fair为true时创建公平锁。
类别 | synchronized | Lock |
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁; 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假如A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,可以尝试继续获取锁,线程不用一直等待 |
锁的状态 | 无法判断 | 可以判断 |
锁类型 | 可重入、不可中断、非公平 | 可重入、可判断、可公平 |
性能 | 少量同步 | 大量同步 |
区别:
//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();...condition.await();...condition.signal();
condition.signalAll();
synchronized 原语和 ReentrantLock 在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。