synchronized 和 volatile

一、synchronized 关键字

1、synchronized 用法

synchronized 关键字可以保证线程的原子性和可见性,它锁的是一个对象,他有如下几种用法

1)对象锁

这里锁的是 object 对象的实例。

public class Demo1 {
    private int count = 10;
    private Object object = new Object();

    public void test(){
            synchronized (object) {
                while (count>0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
        }
    }
    public static void main(String[] args){
        Demo1 demo1 = new Demo1();
        new Thread(()->demo1.test(),"t1").start();
        new Thread(()->demo1.test(),"t2").start();
    }
}
2)this锁

这里使用的是 this 锁,锁的是当前类的实例。

public class Demo2 {

    private int count = 10;
    
    public void test(){

        synchronized (this) {
            while (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
        }
    }
    public static void main(String[] args){
        Demo2 demo2 = new Demo2();
        new Thread(()->demo2.test(),"t1").start();
        new Thread(()->demo2.test(),"t2").start();
    }
}
3)非静态同步函数

直接声明在方法上,相当于相当于是 synchronized(this)

public class Demo3 {

    private int count = 10;

    public synchronized void test(){
        while (count>0) {
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }

    public static void main(String[] args){
        Demo3 demo3 = new Demo3();
        new Thread(()->demo3.test(),"t1").start();
        new Thread(()->demo3.test(),"t2").start();
    }
}
4)静态同步函数

1)synchronized 关键字修饰的静态方法锁的是类的.class 文件;
2)静态方法中,锁定的对象只能是类的.class文件,不能是类的实例;

public class Demo4 {
    private static int count = 10;
    //静态方法中synchronize锁定代码块,锁定的对象不能是类的实例,只能是类的.class文件。
    public synchronized static void test(){
        while (count>0) {
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}
    public static void test2(){
        //这里不能替换成this
        synchronized (Demo4.class){
            while (count>0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            }
        }
    }
    public static void main(String[] args){
        Demo4 demo4 = new Demo4();
        new Thread(()->demo4.test2(),"t1").start();
        new Thread(()->demo4.test2(),"t2").start();
    }
}

2、使用 synchronized 关键字注意事项

1)防止锁对象的引用的改变

当我们定义了一个对象锁,锁住了这个对象 o,如果这个对象的属性发生改变,不会影响锁住的对象,如果这个对象变成了一个新的对象,也就是重新 new 了一次,那锁住的对象就会发生改变,如下例子。

public class Demo1 {

    Object o = new Object();

    public void test(){
        synchronized (o) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        Demo1 demo = new Demo1();

        new Thread(demo :: test, "t1").start();

        Thread t2 = new Thread(demo :: test, "t2");
		//去掉此句 t2 无法执行,否则 t2 能够执行
        demo.o = new Object();
        t2.start();
    }
}
2)不要用字符串常量作为锁的对象

如果使用字符串常量作为锁,在字符串相同的情况下,锁住的则是同一个对象,因为他们的引用都指向常量池的同一个地址。

public class Demo2 {

    String s1 = "hello";
    String s2 = "hello";

    public void test1(){
        synchronized (s1) {
            System.out.println("t1 start...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end...");
        }
    }

    public void test2(){
        synchronized (s2) {
            System.out.println("t2 start...");
        }
    }
    public static void main(String[] args) {
        Demo2 demo = new Demo2();
        new Thread(demo :: test1,"test1").start();
        new Thread(demo :: test2,"test2").start();
    }
}
3)同步代码块中的语句越少越好

在 test1 中,是给整个方法上了锁,但由于我们的业务逻辑只需要锁住的对象是count++,所以在 test2 中我们只给了这个代码上了锁,这样细化了锁的粒度,使代码的效率得以提高。

public class Demo3 {

    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();
        }
    }
}

3、 synchronized 关键字特点

1)同步方法和非同步方法可以同时调用
public class Demo{

    public synchronized void test1(){
        System.out.println(Thread.currentThread().getName() + " test1 start...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " test1 end...");
    }

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

    public static void main(String[] args) {
        Demo demo = new Demo();
        new Thread(demo :: test1,"test1").start();
        new Thread(demo :: test2,"test2").start();
    }
}
2)synchronized支持重入

synchronized支持重入,一个同步方法调用另一个同步方法,也可以得到锁。

public class Demo {

    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) {
        Demo demo= new Demo();
        demo.test1();
    }
}
3)异常情况会自动释放锁

如果碰到异常情况,当先线程会自动释放锁

public class Demo {

    int count = 0;

    synchronized void test(){
        System.out.println(Thread.currentThread().getName() + " start......");
        while (true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //碰到异常的情况,如果没有处理,会自动释放锁,所以T2可以执行。
                int i = 1/0;
            }
        }
    }

    public static void main(String[] args) {
        Demo demo11 = new Demo();

        Runnable r = () -> demo11.test();

        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }
}

二、volatile 关键字

1、volatile 关键字特点

1)保证可见性

什么是可见性,在JMM(Java 内存模型) 中分为主存和本地私有内存,主存中存放的是所有线程的共享数据,本地内存中存放的是某个线程的私有数据,在某一线程启动时,会先去读取主内存的数据到本地私有内存,然后再运行这个副本,但并不会每次都去读取,所以会造成数据不一致问题。

volatile 关键字正是为了解决这个问题,我们通过以下代码举例。
在这里插入图片描述
以下代码,在不添加 volatile 关键字前,虽然 running 改变了,但线程 t1 仍然不会停止,是因为虽然主内存改变了,但本地私有内存并不知道。 当添加了 volatile 关键字后,每一次被 volatile 关键字修饰的变量的值发生改变时,主内存都会通知本地私有内存进行修改,所以线程 t1 可以结束。

public class Demo {

    /*volatile*/ boolean running = true;

    public void test(){
        System.out.println("test start...");
        while (running){
        }
        System.out.println("test end...");
    }
    public static void main(String[] args) {
        Demo demo = new Demo();

        new Thread(demo :: test,"t1").start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        demo.running = false;
    }
}
2)不保证原子性

如下代码,多个线程对一个共享变量进行累加,比如一个线程 A 加到了100,但是还没有往上添加,另一个线程 B 也来了,它也把 100 拿过来进行累加,他们两都加了 1 ,于是把结果101 存了进去,虽然是加了 2 次,但实际上只加了 1 次,所以 volatile关键字不保证原子性,它不能代替Synchronized。

public class Demo {

    volatile int count = 0;

    public void test(){
        for (int i = 0; i < 10000; i++) {
            count ++;
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        List<Thread> threads = new ArrayList();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test, "thread-" + i));
        }
        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        System.out.println(demo.count);
    }
}

当我们在方法上添加了 synchronized ,发现最终结果为 10000,说明 synchronized 既保证了可见性,又保证了原子性。

public class Demo {

    int count = 0;

    public synchronized void test(){
        for (int i = 0; i < 10000; i++) {
            count ++;
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(demo::test, "thread-" + i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        System.out.println(demo.count);
    }
}

我的 Github:Github
CSDN: CSDN
个人网站: sirius 的博客
E-mail: [email protected]

推荐阅读
史上最全,最完美的 JAVA 技术体系思维导图总结,没有之一!

发布了81 篇原创文章 · 获赞 373 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/Sirius_hly/article/details/97781722
今日推荐