synchronized使用解析(3)

x33g5p2x  于2021-08-23 转载在 Java  
字(3.5k)|赞(0)|评价(0)|浏览(119)

一 线程数据共享问题

1.1 线程共享安全问题的产生

多个线程对同一个类的普通成员进行非原子性操作就会造成线程共享的安全问题。示例如下,我们使用一个类实现Runnable接口,在这个类中定义了计数值count,当count的值小于最大值maxCount时,计数值就会加1。我们用测试类分别启动四个线程操作这个类。

实现类:

/**
 * @Author lsc
 * @Description <p> </p>
 * @Date 2019/10/22 20:44
 * @Version 1.0
 */
public class SysYouku1 implements Runnable {

    int count = 0;

    int maxCount = 200;

    @Override
    public void run() {
        while (count<maxCount){
            System.out.println(Thread.currentThread().getName()+" -  "+count++);
             
        }
    }
}

测试类:

  /*
     * @Author lsc
     * @Description  <p> 多线程安全问题</p>
     * @Date 2019/10/22 20:53
     * @Param []
     * @return void
     **/
    @Test
    public void syncronizedTest1(){

        SysYouku1 sysYouku1 = new SysYouku1();
        Thread t1 = new Thread(sysYouku1, "youku1");
        Thread t2 = new Thread(sysYouku1, "youku2");
        Thread t3 = new Thread(sysYouku1, "youku3");
        Thread t4 = new Thread(sysYouku1, "youku4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }

输出结果:

youku4 -  132
youku4 -  196
youku4 -  197
youku4 -  198
youku4 -  199
youku2 -  195
youku3 -  184
youku1 -  177

发现这些数据无序,并且会造成缺省等情况,这是由于我们在类中共享变量,cpu调度轮询引起的。在这个过程中会发生三种问题,情况如下。

1.2 重复问题

当两个线程都进入run方法并且依次取到count的值(同一个值),thread 1将count数值+1然后cpu将控制权交给thread2,thread将count的值也加1。thread 1 和 thread2 ,将改变的count值 赋值,然后依次输出,造成了数据重复问题。
在这里插入图片描述

1.3 超值问题

thread1 和 thread2 都进入run方法,thread1 操作了 count 加1 并且将操作后的数值赋值,thread2 此时拿到的count 是 thread1 操作后的值,再次基础上 thread再次将值 加1,造成超值问题。
在这里插入图片描述

1.3 省略问题

thread1 , thread2 同时进入run 方法,thread 1 对 count 进行操作数值加1 输出,thread2此时拿到count的值操作加1停顿,thread1再次进入在thread2操作的基础上对count加1,输出。
在这里插入图片描述

1.4 线程安全问题简述

线程安全问题的产生是在多线程对这个类的进行访问时,这个类表现出了不正确的行为。大量的线程安全问题都是对变量的操作非原子性和数据共享造成的,如上的例子,对count这个成员变量进行了非原子性操作,在时序上出现了混乱,我们称这种情况是竞争条件(race condition

1.5 线程安全问题解决思路

  1. 不共享成员变量
  2. 对类或成员变为不可变的对象
  3. 使用同步机制

当然字段也可以使用ava.util.concurrent.atomic包中的关键字修饰解决,比如AtomicInteger,本文主要讲解synchronized同步机制,具体如下文。

二 synchronized 指南(重点)

2.1 synchronized 定位

  1. 多个线程是不能同时调用同一个对象用synchronized 修饰的方法,当其中一个线程在执行这个通同步方法时,其余线程都处于阻塞状态,直到这个线程完成对这个对象的操作
  2. 当一个同步方法退出时,它会自动与之前的调用过同步方法的对象建立一种 happens-before 关系。这保证了所有线程能够对这个对象的状态改变都是可见的。
  3. synchronized方法支持一种简单的策略用于防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都通过synchronized方法完成,那么这个策略就会生效,但是会造成性能问题(final修饰的成员变量除外,可以安全的直接调用)

2.2 锁机制介绍

  1. Synchronization 是围绕着内部被称为内部锁或者监管锁的实体建立的(API规范通常简单将这个实体称为监视器),内部锁在Synchronization 中扮演着重要的作用:强制独占一个对象的状态并且建立一种对外必须是可见的happens-before 关系。
  2. 每一个对象都拥有一个与自身关联的内部锁(Monitor Lock);按照惯例,一个线程要独占和一致性的访问一个对象的字段必须在进入之前获得这个对象的内部锁,当结束对这个对象的操作时就会释放锁。一个线程拥有内部锁的时期是从获得锁到释放锁。只要一个线程拥有内部锁,其他线程都不能获得同样的锁;当其他线程试图去获取这个锁就会陷入阻塞状态

三 synchronized 的 用法

3.1 用在对象的方法上

官网描述如下,意指当一个线程调用这个同步方法,这个线程会自动获得拥有这个方法的对象的内部锁,并且在方法返回的时候释放锁;即使在返回的时候出现了未捕获的异常任然会释放这个锁。

示例:

    public synchronized String sync(){
        // do nothing
        return  null;
    }

3.2 用在类的静态方法上

官网描述如下,意指 静态同步方法修饰的锁是与类相关联的不是这个对象;访问这个类的静态字段是被这个锁所控制,这个锁与任何类的实例的锁都不相同。

示例:

public static synchronized String syncS(){
        // do nothing
        return  null;
    }

3.3 用在同步代码块上

官网描述如下,意指同步代码块必须要有一个具体的对象提供其内部锁

示例:

 public  String syncy(){
        // do nothing
        synchronized (this){
            
        }
        return  null;
    }

四 synchronized 用法的注意事项

  1. 避免在同步代码块里面调用其它对象的方法
  2. synchronized 应尽量只作用于共享变量
  3. 在使用同步代码块时与monitor关联的对象不能为null
  4. 避免交叉锁出现死锁
  5. 相互调用同步方法

五 示例分析synchronized用在同步代码块上

5.1 示例代码清单

含有同步方法的类:

/**
 * @Author lsc
 * @Description <p> </p>
 * @Date 2019/10/23 22:08
 * @Version 1.0
 */
public class Monitor {

    private int count = 0;

    public  int sync(){

        synchronized(this){
            count++;
            try {
                TimeUnit.SECONDS.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return count;
    }
}

主类使用Monitor 对象启动两个线程:

    public static void main(String[] args) {

        Thread t1 = new Thread(new Monitor()::sync, "youku1");
        Thread t2 = new Thread(new Monitor()::sync, "youku2");
        t1.start();
        t2.start();
    }

5.2 字节码反汇编分析

可以证明上述中一个对象只有一个Monitor(监视器),在同一个时刻,只能有一个线程获得lock进入monitor,当释放lock时出monitor(也就是方法返回的时候)
在这里插入图片描述

5.3 jstack 运行结果

可以看见当youku2线程进入睡眠时候,已经上锁了,此时其他线程是无法进入的。
在这里插入图片描述

相关文章