synchronized和volatile学习总结

synchronized

用法:修饰方法或者修饰代码块

1、修饰方法
     1.1、修饰普通方法
     1.2、修饰静态方法
2、修饰代码块

性质:

     1、具有原子性和可见性
     2、同步方法可以调用另外一个同步方法(synchronize是可重入锁)
     3、非同步方法和同步方法可以同时调用
     4、子类的同步方法可以调用父类的同步方法
     4、synchronized遇到异常默认是释放锁资源(并发情况需要特别留意)

使用优化:

     1、同步代码越少越好,只锁涉及到同步的部分(提高效率)
     2、锁对象的属性发生改变,不会影响锁的使用,但是如果将引用指向另外的对象,那么锁就会发生改变——避免将锁对象的引用指向另外的对象
     3、不要以字符串常量作为锁定对象

用法:

1、修饰普通方法——相当于修饰代码块(锁定的是this对象)

说明:每个对象都有一个内置锁,对一个对象加锁,线程在执行的时候,去堆内存中请求该对象的锁(保存在堆内存中),获取到该对象的锁之后才能执行同步代码块/同步方法。
注意:锁和同步代码并没有什么直接关系,可以直接创建一个对象作为锁对象——用于竞争获取,而同步代码可以和该锁可以没有关系。
下面2种写法的效果一致
public class T{
	private int count = 10;
	
	//修饰普通方法——锁定this对象
	public synchronized void f1(){
		count--;
		System.out.println(Thread.currentThread().getName() + "count=" +count);
	}
	//修饰代码块——锁定this对象
	public void f2(){
		synchronized(this){
			count--;
			System.out.println(Thread.currentThread().getName() + "count=" +count);
		}
	}

}


2、修饰静态方法——锁定该类的class字节码对象

下面两种写法的效果一样
public class T{
	private static int count = 10;

	//修饰静态方法
	public static synchronized void f1(){
		count--;
        System.out.println(Thread.currentThread().getName() + "count= " +count);
	}

	//相当于如下操作——锁定T.class对象
	public static void f2(){
		synchronized(T.class){
			count--;
        	System.out.println(Thread.currentThread().getName() + "count= " +count);
		}
	}

}

性质

1、原子性和可见性

在没有对run()进行加锁(synchronized)的情况下,会出现重复的值——因为没有加锁,所以多个线程同时运行,出现的结果不唯一。
但是在加锁之后,一次只能有一个线程执行run()方法,所以输出的结果是9 8 7 6 5
Thread0count= 9
Thread3count= 8
Thread2count= 7
Thread1count= 6
Thread4count= 5
这里Thread的名字并不是按照0 1 2 3 4 这样排的,因为一次性创建了5个线程,但是这些线程谁获取到该对象的锁并且执行run()方法,
完全是由cpu调度来决定的,这个具有随机性。

public class T  implements Runnable{
    private int count=10;

    public  /*synchronized */void run(){
        count--;
        System.out.println(Thread.currentThread().getName() + "count= " + count);
    }

    public static void main(String[] args) {
        T t=new T();
        for (int i=0;i<5;i++){
            new Thread(t,"Thread"+i).start(); 
        }
    }
}

2、非同步方法和同步方法可以同时调用


同步方法是需要获取到该对象的锁,但是非同步方法并不需要获取该对象的锁,因而是没有影响的;可以同时调用
一个比喻:比如说同步方法就相当于是上厕所,需要获得厕所门的锁,但是非同步方法就好比是在厕所坑外面清洁的人,
并不需要获取厕所门的锁。


public class T {

	//锁定普通的方法 就相当于 synchronized(this) 锁定该对象 也就是t
    public synchronized void f1(){  
        System.out.println(Thread.currentThread().getName() + "m1 start....");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m1 end .....");
    }
	//非同步方法
    public void f2(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m2 end...." );
    }

    public static void main(String[] args) {
        T5 t=new T5();
        new Thread(()->t.f1(),"t1").start();
        new Thread(()->t.f2(),"t2").start();
    }
}
/*
运行结果:
m1 start....
m2 end....
m1 end.....
*/

3、一个同步方法可以调用另外的一个同步方法(synchronized是可重入锁)

一个同步方法可以调用另外的一个同步方法,一个线程已经拥有了某个对象的锁,再次申请的时候仍然会获取该对象的锁
相当于锁定了2次 同一线程 同一把锁 所以能 也即:同一个线程可以多次锁定同一个对象
public class T {

	//同步方法1
    public synchronized void f1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //调用f2()
        f2();
    }
    //同步方法2
    public synchronized void  f2(){

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(()->t.f1()).start();
    }

}


4、子类的同步方法调用父类的同步方法

锁定的是同一个对象:子类的对象

class T extends T7{

    @Override
    synchronized void f1(){
        System.out.println("child  start");
        super.f1(); //子类的同步方法可以调用父类的同步方法
        System.out.println("child end");
    }
}

public class T7 {

    synchronized void f1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2  end");
    }

    public static void main(String[] args) {
        new T().f1();
    }
}

/*
运行结果:
child  start
m1 start
m2  end
child end
*/

5、synchronized在运行的过程中遇到异常是默认会释放锁

在处理并发的情况下,要额外的小心异常的处理,如果异常处理不好,会导致释放锁之后数据的不一致
例如:
在一个webAPP中,多个servlet线程访问同一块资源,如果某个servlet线程发生了异常,如果异常处理不合适,那么会导致数据有问题。
因为线程在执行的过程中遇到异常,自动释放锁之后,这个时候可能出现非原子性(遇到异常的servlet线程的操作并没有完成)。
spring有事务管理,就是在并发的情况下,如果遇到异常会自动回退到原始状态。

例子:
2个线程同时启动竞争对象锁,其中一个竞争成功后,对count一直进行++的操作,当count==5的时候,发生了异常,这个时候回默认
释放锁资源,然后另外一个线程就会获取到锁资源并且执行代码,但是!!!这个时候count是从5开始的,但是这个5是上一个线程
并未完成所有操作而产生的数据,因而该数据是有问题的!
public class T8 {
    int count=0;
    synchronized void f1(){
        System.out.println(Thread.currentThread().getName() +  " satrt");
        while (true){
            count++;
            System.out.println(Thread.currentThread().getName() + "count= " +count );
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5){
                int i=1/0;  //模拟会有异常
            }
        }
    }

    public static void main(String[] args) {
        T8 t8=new T8();
        for (int i=0;i<2;i++){
            new Thread(()->t8.f1()).start();
        }
    }
}

volatile

性质

    可见性——使一个变量在线程之间可见

背景介绍:cpu和内存在运行速度上有很明显的差异(相差100倍左右),为了缓解运行速度上的矛盾,在cpu中设置一个缓存区,缓冲区存放着从主存(内存)中读取需要操作的数据,但是多线程的情况下(每个cpu都会有自己的缓存),当某一个cpu线程中对缓存数据的修改之后,其他的cpu缓存中并不会立刻更新数据(默认情况下,具体何时进行更新操作得看cpu什么时候空闲),所以如果多线程下其中一个线程对数据进行了修改,其他的线程并不会立刻更新数据,因而在数据上就会有问题(互相不可见);因而出现了volatile,volatile的作用就是:当某个cpu的缓存对数据进行修改之后,会立刻写到主存(内存)中,并且会通知其他的线程及时的更新数据,以获取最新的数据。
在把volatile注释掉的情况下,线程1不会结束(起码短时间内不会,可能在cpu空闲的时候会自动的去主存中更新数据)。
因为线程1中缓存的flag还是true,没有使用volatile及时更新缓存数据导致。

public class T {

    private /* volatile */ boolean flag=true;

    void f1(){
        System.out.println("m1 start....");
        while (flag){
        }
        System.out.println("m1 end....");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(()->t.f1()).start();
        //主线程休眠1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //主线程修改flag
        t9.flag = false;
    }
}

volatile不具有原子性

volatile不具备原子性,并不能保证多个线程共同修改变量的时候所带来的不一致问题,因而不能替代synchronized
public class T {
    private volatile int count=0;
    public void m(){
        for (int i=0;i<10000;i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for (int i=0;i<10;i++){
            threads.add(new Thread(t::m," thread "+i));
        }
        threads.forEach((o)->o.start());
        threads.forEach((o->{
            try {
                o.join(); //线程让步  等待其他线程执行完
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
        System.out.println(t10.count);
    }
}
结果是小于10W的,假设线程1对count进行操作,加到了100之后刷新了主存,而线程2获取到了100之后,对count++操作到150之后提交,
但是线程3这个时候可能只++101,然后提交,就会覆盖线程2150

synchronized和volatile的区别

  1、synchronized具有原子性和可见性,而volatile只有可见性

  2、volatile仅能使用在变量级别,synchronized则可以使用在变量和方法上

  3、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.

  4、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值