IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Synchronized的使用和优化 -> 正文阅读

[Java知识库]Synchronized的使用和优化

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  1. 普通同步方法,锁的是当前实例对象;
  2. 静态同步方法,锁的是当前类的class对象;
  3. 同步方法块,锁的是括号里面的对象。

场景 1、锁对象的改变

锁定某对象 o,如果 o 的属性发生改变,不影响锁的使用,但是如果 o 变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外一个对象。

public class Sync1 {

    Object o = new Object();

    public void sync() {
        synchronized (o) {
            //t1拿到锁 在这里无限执行,并没有走出同步代码块
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("当前线程" + Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        Sync1 sync1 = new Sync1();
        Thread t1 = new Thread(sync1::sync, "t1");
        t1.start();
        Thread t2 = new Thread(sync1::sync, "t2");
        t2.start();
    }
}

输出:
当前线程t1
当前线程t1
当前线程t1
当前线程t1
......

如果改变对象 o,则2个线程交替执行。

public static void main(String[] args) {
    Sync1 sync1 = new Sync1();
    Thread t1 = new Thread(sync1::sync, "t1");
    t1.start();
    Thread t2 = new Thread(sync1::sync, "t2");
    //改变对象 o
    sync1.o = new Object();
    t2.start();
}

输出:
当前线程t1
当前线程t2
当前线程t1
当前线程t2
当前线程t2
当前线程t1
......

场景 2、字符串作为锁定对象

不要以字符串常量作为锁定的对象

public class Sync2 {

    String lock1 = "lock";
    String lock2 = "lock";

    public void sync1() {
        synchronized (s1) {
            //t1 在这里无限执行
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("当前线程" + Thread.currentThread().getName());
            }
        }
    }
    public void sync2() {
        synchronized (s2) {
            System.out.println("当前线程" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        Sync2 sync1 = new Sync2();
        Thread t1 = new Thread(sync1::sync1, "t1");
        t1.start();
        Thread t2 = new Thread(sync1::sync2, "t2");
        t2.start();
    }
}

输出:
当前线程t1
当前线程t1
当前线程t1
当前线程t1
......

可以看到线程 1 和 2 分别锁的是 lock1 和 lock2,而执行结果确还是被线程 1 阻塞,虽然表面上看并不是同一个对象,但实际上我们知道在JVM堆中的常量池中只有一个字面量"lock",即lock1 == lock2 = true

因此,在实际开发中我们无法保证别人也用到相同字面量的锁,一旦相同,后果就非常严重了。

场景 3、减小锁的粒度

什么是锁的粒度呢?所谓锁的粒度就是你要锁住的范围是多大。

比如你在家上卫生间,你只要锁住卫生间就可以了,不需要将整个家都锁起来不让家人进门吧,卫生间就是你的加锁粒度。

怎样才算合理的加锁粒度呢?

其实卫生间并不只是用来上厕所的,还可以洗澡,洗手。这里就涉及到优化加锁粒度的问题。

你在卫生间里洗澡,其实别人也可以同时去里面洗手,只要做到隔离起来就可以,如果马桶,浴缸,洗漱台都是隔开相对独立的,实际上卫生间可以同时给三个人使用,当然三个人做的事儿不能一样。这样就细化了加锁粒度,你在洗澡的时候只要关上浴室的门,别人还是可以进去洗手的。如果当初设计卫生间的时候没有将不同的功能区域划分隔离开,就不能实现卫生间资源的最大化使用。

比较test1test2,业务逻辑中只有count++这句需要同步,这时不应该给整个方法上锁采用细粒度的锁,同步代码快中的语句越少越好,可以使线程争用时间变短,从而提高效率。

public class Sync3 {

    int count = 0;

    public synchronized void test1() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        count++;

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /**
     * 局部加锁
     */
    public void test2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized (this) {
            count++;
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

场景 4、锁粗化

在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。这也是很多人原因是用同步代码块来代替同步方法的原因,因为往往他的粒度会更小一些,就和上面讲的一样。

但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

就好比你去银行办业务,你为了减少每次办理业务的时间,你把要办的三个业务分成三次去办理,这反而适得其反了。因为这平白的增加了很多你重新取号、排队、被唤醒的时间。

public void doSomething() {
    synchronized (lock) {
        //业务1
    }
    //do other some thing
    synchronized (lock) {
        //业务2
    }
    synchronized (lock) {
        //业务3
    }
}

实际上,一个柜台是可以处理多个业务的

public void doSomething() {
    synchronized (lock) {
        //业务1
        //do other some thing
        //业务2
        //业务3
    }
}

另一种需要锁粗化的极端的情况是:加锁操作是出现在循环体中

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
    }   
}  

上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

锁粗化后的代码如下:

synchronized(this){  
    for(int i=0;i<100000;i++){  
        do(); 
    }
}  

场景 5、锁消除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

怎么理解?比如方法内局部申明锁对象:

public void sync() {
    Object o = new Object();
    synchronized (o){
        do();
    }
}

在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。

当然,这种情况我们都是能看出来的,也不会这么写,万一写了这样的代码那只能 kill 一个程序猿祭天了。

另外,我们知道StringBuffer经常用来拼接字符串,而且append()方法是线程安全的,查看源码可以看到该方法是通过synchronized修饰的:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

如果我们在线程内部把StringBuffer当作局部变量使用:

for (int i = 0; i < 10000; i++) {
    StringBuffer str = new StringBuffer();
    str.append("Java");
    str.append("tv");
}

如果你没看过append()方法的源码,也不知道啊,所以在这种情况下,JIT就可以帮忙优化,进行锁消除。

场景 6、同步方法和非同步方法同时调用

执行一个同步方法,在没有释放锁的情况下,不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响)。

public class Sync {


    public synchronized void test1() {
        System.out.println(Thread.currentThread().getName() + " test1 start...");
        try {
            //睡眠5s 由于还要t2要执行 cpu回去执行t2
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " test1 end...");
    }

    public void test2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " test2");
    }

    public static void main(String[] args) {
        Sync sync = new Sync();
        //正在执行一个同步方法  没有释放锁
        new Thread(sync::test1, "t1").start();
        //不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响)
        new Thread(sync::test2, "t2").start();
    }
}

输出:
t1 test1 start...
t2 test2
t1 test1 end...

场景 7、锁重入

一个同步方法调用另外一个同步方法,是可以获取到锁的,synchronized默认支持重入。

synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。

可重入可以避免一些死锁的情况,也可以让我们更好封装我们的代码。

public class Sync {

    synchronized void test1() {
        System.out.println("test1 start...");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test2();
    }

    synchronized void test2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test2 start...");
    }


    public static void main(String[] args) {
        Sync sync = new Sync();
        sync.test1();
    }
}

输出:
test1 start...
test2 start...

这里要注意test2为什么也需要加synchronized

是因为你无法保证别的线程来单独调用test2

场景 8、synchronized和exception

synchronized 锁定一段代码之后,如果在同步代码块中遇到异常,会自动释放锁。

public class Sync {
    Object o = new Object();

    int count = 0;

    void test() {
        synchronized (o) {
            //t1进入并且启动
            System.out.println(Thread.currentThread().getName() + " start...");
            //t1 会死循环 t1 讲道理不会释放锁
            while (true) {
                count++;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //加5次之后 发生异常
                if (count == 5) {
                    int i = 1 / 0;
                }
            }
        }
    }

    public static void main(String[] args) {
        Sync demo11 = new Sync();
        new Thread(() -> {
            demo11.test();
        }, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            demo11.test();
        }, "t2").start();
    }
}

输出:

t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
Exception in thread "t1" java.lang.ArithmeticException: / by zero
	at cn.javatv.sync.used.demo8.Sync.test(Sync.java:31)
	at cn.javatv.sync.used.demo8.Sync.lambda$main$0(Sync.java:40)
	at java.lang.Thread.run(Thread.java:745)
t2 start...
t2 count = 6
t2 count = 7
t2 count = 8
t2 count = 9
......

可以看到抛出异常后会释放锁,这是synchronized 的机制,在遇到异常后会gotomonitorexit

image-20210923165830441

需要注意的是,如果异常被try catch那么是不会释放锁的,把上面的代码改动一下:

try {
    if (count == 5) {
        int i = 1 / 0;
    }
} catch (Exception e) {
    e.printStackTrace();
}

输出:

t1 start...
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
java.lang.ArithmeticException: / by zero
	at cn.javatv.sync.used.demo8.Sync.test(Sync.java:32)
	at cn.javatv.sync.used.demo8.Sync.lambda$main$0(Sync.java:44)
	at java.lang.Thread.run(Thread.java:745)
t1 count = 6
t1 count = 7
t1 count = 8
t1 count = 9
t1 count = 10
t1 count = 11
t1 count = 12
......

可以看到一直是t1获取到锁。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-24 10:24:23  更:2021-09-24 10:25:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 18:25:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码