多角度简单解析synchronize关键字
这里写自定义目录标题
Synchronized简介
作用
保证在同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。其主要通过拿到锁来保证执行代码块的线程唯一性,只有拿到锁的线程才能执行synchronize中的代码。
不使用synchronize的后果
我们可以启动两个线程对同一个变量进行自增的操作:
public class HowToUseSyn implements Runnable{
static HowToUseSyn howToUseSyn = new HowToUseSyn();
// 计数器
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(howToUseSyn);
Thread threadTwo = new Thread(howToUseSyn);
threadOne.start();
threadTwo.start();
// 保证线程1,2都能执行完在打印结果
Thread.sleep(2000);
System.out.println(count);
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
add();
}
}
// 提供自增的方法
public static void add(){
count++;
}
}
结果我们可以看到,所得的值并不是我们期望的加了20W次(因为每个线程都加了10W次)。
原因:conut++
看上去是一个操作,其实其包含了三个操作:
1、首先获取conut变量的值
2、然后对count变量的值进行+1的操作
3、最后在将count的值写到内存中。
这就导致了,当线程一获取到了conut值并且已经进行了+1的操作,但是并未写入到内存中,此时线程二进来了并读取了未修改的conut的值。所以在这样的情况下两个线程对变量进行了相同数值的操作。
两种用法
对象锁
1.方法锁(默认锁对象为this当前实例对象)
synchronize修饰普通方法,锁对象默认为this
public class DuiXiangSuoTwo implements Runnable{
static DuiXiangSuoTwo duiXiangSuo = new DuiXiangSuoTwo();
@Override
public void run() {
method();
}
// 加了synchronize的同步方法
public synchronized void method(){
System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
}
public static void main(String[] args) {
Thread thread1 = new Thread(duiXiangSuo);
Thread thread2 = new Thread(duiXiangSuo);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
}
}
2.同步代码块锁(自己指定锁对象)
手动指定锁对象
锁this对象代码实例:
public class DuiXiangSuo implements Runnable{
static DuiXiangSuo duiXiangSuo = new DuiXiangSuo();
@Override
public void run() {
synchronized (this){
System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(duiXiangSuo);
Thread thread2 = new Thread(duiXiangSuo);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
}
}
从输出结果我们可以看到,线程0和线程1都是串行的进行代码块并执行其中的内容。这就很好的保证了在synchronize代码块中最多只有一个线程执行其中的代码。
不同的线程拿不同的锁去执行不同的任务:
当我们需要多个线程去协作不同的任务的时候,我们就不能锁住this对象,而是自己创建一个对象锁,让synchronize去锁我们创建的对象。
代码实例(使用this锁):
public class DuiXiangSuo implements Runnable{
static DuiXiangSuo duiXiangSuo = new DuiXiangSuo();
// 锁1
Object lock1 = new Object();
// 锁2
Object lock2 = new Object();
@Override
public void run() {
synchronized (this){
System.out.println("线程"+Thread.currentThread().getName()+"拿到锁1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁1");
}
synchronized (this){
System.out.println("线程"+Thread.currentThread().getName()+"拿到锁2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁2");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(duiXiangSuo);
Thread thread2 = new Thread(duiXiangSuo);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
}
}
我们看结果可以知道,执行了两个任务锁this的话,会让另一个任务暂时的停止。不能让两个线程同时开始任务,而再看,我们更换了不同对象锁以后:
public class DuiXiangSuo implements Runnable{
static DuiXiangSuo duiXiangSuo = new DuiXiangSuo();
// 锁1
Object lock1 = new Object();
// 锁2
Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1){
System.out.println("线程"+Thread.currentThread().getName()+"拿到锁1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁1");
}
synchronized (lock2){
System.out.println("线程"+Thread.currentThread().getName()+"拿到锁2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁2");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(duiXiangSuo);
Thread thread2 = new Thread(duiXiangSuo);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
}
}
从结果我们可以看到,两个线程可以同时拿不同的锁去执行不同的同步代码块,这就大大加快的线程之间协作的效率。
类锁
Java类有可能会有多个对象,但是Class类对象只有一个;所以所谓类锁,不过是Class对象的锁而已,但是Class对象只有一个,所以就能保证同一时刻只有一个线程获取该Class类对象的锁。
1、synchronize修饰静态的方法
我们来看看不同对象不加synchronize的情况:
public class ClassSuoStaticMethod implements Runnable{
static ClassSuoStaticMethod classSuoStaticMethod = new ClassSuoStaticMethod();
static ClassSuoStaticMethod classSuoStaticMethod2 = new ClassSuoStaticMethod();
public static void main(String[] args) {
System.out.println("classSuoStaticMethod=>>"+classSuoStaticMethod);
System.out.println("classSuoStaticMethod2=>>"+classSuoStaticMethod2);
Thread thread1 = new Thread(classSuoStaticMethod);
Thread thread2 = new Thread(classSuoStaticMethod2);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
System.out.println("finish!");
}
@Override
public void run() {
method();
}
public synchronized void method(){
System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
}
}
可以看到,即使是加了锁,两个线程是都可以并行执行的。因为加的是不同对象的锁,两个线程获取的对象锁都是不同的,所以他们可以并行的执行。
加入static后:
从结果我们可以看出,线程串行的执行了。即方法加入了static后,锁住的便是该类对象,所以即使两个的实例对象不一样,但是其实有类对象new出来的,所以还是会进行互斥,从而达到了串行执行的效果。
2、指定锁为Class对象
同样的,我们先来看锁this的情况(也就是锁当前对象实例)
public class ClassSuoStaticClass implements Runnable{
static ClassSuoStaticClass classSuoStaticMethod = new ClassSuoStaticClass();
static ClassSuoStaticClass classSuoStaticMethod2 = new ClassSuoStaticClass();
public static void main(String[] args) {
System.out.println("classSuoStaticMethod=>>"+classSuoStaticMethod);
System.out.println("classSuoStaticMethod2=>>"+classSuoStaticMethod2);
Thread thread1 = new Thread(classSuoStaticMethod);
Thread thread2 = new Thread(classSuoStaticMethod2);
thread1.start();
thread2.start();
while(thread1.isAlive() || thread2.isAlive()){}
System.out.println("finish!");
}
@Override
public void run() {
method();
}
public void method(){
// 每个线程都会执行到这个方法,锁this就是锁这个线程实例对象,对另一个线程不构成任何干扰
synchronized(this) {
System.out.println("线程" + Thread.currentThread().getName() + "进入代码块");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "结束任务");
}
}
}
我们可以看到,锁this的话,不同线程之间不会进行互斥,都可以拿到锁进入到同步代码块中。
而我们再看锁对象:
从结果中我们看出,锁对象的话不同线程之间就会进行互斥。可以保证在同一时间下只有一个线程执行同步代码块的内容。
多线程访问同步方法的7种情况
1、两个线程访问同一个对象的同步方法:
争抢同一把锁,只有一个线程能拿到锁去执行。
2、两个线程访问两个对象的同步方法:
synchronize锁的是不同的对象实例,所以两个线程不会产生互斥,并行的执行代码。
3、两个线程访问synchronize的静态方法:
争抢同一把类锁,只有一个线程能拿到锁去执行。
4、同时访问同步方法和非同步方法:
非同步方法不会受到影响,synchronize对哪个方法加锁那么只针对哪个方法有效,其他的方法不会产生影响。
5、访问同一个对象的两个不同的同步方法
既然是同一个对象,那么synchronize加锁指向的this也是指向同一个,所以会导致程序串行的执行。
6、同时访问静态synchronize和非静态synchronize
非静态synchronize锁的是this对象实例,而静态synchronize锁的是Class这个唯一的对象。这两个锁的不是同一个,所以会让程序并行的执行。
7、方法抛异常后,会释放锁
JVM会帮忙释放锁。
还有一种特殊的情况:在同步方法中调用非同步方法,此时已经线程不安全。该非同步方法其他的线程也可以进行直接访问,所以最终结果是多个线程并行访问该非同步方法。
七种情况小结:
1、一把锁只能同时被一个线程获取,没有拿到锁的线程只能进行等待
2、每个实例都对应自己的一把锁,不同实例之间互不影响。但是,如果是类锁的话,那么只有一把,*.class或者是静态synchronize方法都使用同一把类锁。
3、不管是正常执行完还是抛出异常,都会释放锁。
Synchronized的性质
1、可重入
含义:外层函数获得锁后,内层函数可以直接再次获得该锁。
不可重入:当需要拿另一把锁的时候,需要释放掉该锁并且重新竞争才可以。
好处:避免死锁,提升封装性
粒度:线程范围而非调用范围。
可重入测试:
2、不可中断
含义:当锁被别的线程获得后,如果我还想获得那么只能进行等待或者阻塞,直到别的线程释放锁。如果别的线程不释放,我将一直等待阻塞。
Synchronized原理
加锁和释放锁的原理
时机:内置锁
从字节码反编译文件看出synchronize:
1、首先,写一个简单的带有synchronize代码块的类:
public class LockSync {
private Object object = new Object();
public void add(){
synchronized (object){
}
}
}
2、通过javac进行编译:
3、通过javap -verbose 编译的类查看所有内容;定位到同步代码块的方法:
此致,我们可以看到了加锁和释放锁的方法;当进入到同步代码块就必须要获得monitor锁的对象,但是释放锁可以让线程执行完也可以是抛出异常。所以,获取锁和释放锁并不是一一配对的。
解读Monditorenter和Monditorexit指令:
Monditorenter和Monditorexit会在执行的时候让对象的锁计数进行+1或者-1的操作,每个对象与一个Monditorenter相关联,而一个Monditorenter的lock锁只能在同一时间被同一个线程获得;一个线程在尝试获得与一个对象关联的Monditorenter所有权的时候,只会发生以下三种情况。1、当前计数器为0,表示并未有线程获得,此时该线程会马上获得并把计数器+1;2、如果重入的话,计数器就会累加;3、Monditorenter被其他线程持有,我去获取他只能进入阻塞状态进行等待,直到Monditorenter计数器变为0。Monditorexit,拥有锁的话,释放其所有权;释放的过程:直接将计数器-1即可。
可重入原理
依靠加锁次数计数器。JVM负责跟踪对象被加锁的次数:线程第一次给对象加锁的时候,计数变为1。每当这个线程在此对象上再次获得锁时,计数会递增。当任务离开时,计数递减,当计数为0的时候,锁被完全释放。
保证可见性原理
简单了解Java内存模型:
线程之间的简单通信:
当一个线程要与另一个线程通信的时候,首先他会将该线程中的本地内存里面的变量写入到主内存中;接着另一个线程在从主内存中读取。使用synchronize关键字,其会在释放锁之前将本地内存的内容写入到主内存中,这样就可以保证在线程A释放锁后和线程B拿到锁之前的这段时间,本地内存和主内存的数据都是一致的。
Synchronized的缺陷
1、效率低
锁的释放情况少:只有代码执行完和抛出异常才能释放。
尝试获取锁不能设置时间
不能中断正在尝试获取锁的线程
2、不够灵活
加锁和释放的时机单一,每个锁只有单一的条件;而读写锁就比较灵活,读数据就不需要加锁,写数据就加上写锁。
3、无法知道是否成功获取锁
Lock接口可以对是否成功获取锁进行下一步业务处理。
常见面试问题
1、synchronize使用注意点:
锁对象不能为空:锁的信息存在对象头中,如果对象都不存在,那么锁信息无法放置。
作用域不宜过大:
避免死锁。
2、Lock和synchronize如何选择:
如果可以,使用JUC包下的类;其次优先使用synchronize;最后,根据业务要写Lock或者使用其方法的时候才用Lock。
3、多线程访问同步方法的具体情况(就是上面那七种)
本文地址:https://blog.csdn.net/a760352276/article/details/107620307