共享模型之管程 synchronized详解

目录

引子

临界区cs

synchronized

语法

线程八锁

变量的线程安全

常见线程安全类

线程安全类方法的组合

不可变类线程安全性

卖票问题

转账问题


引子

private static int cs=0;//全局临界资源
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            cs++;
        }
    });
     Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            cs--;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("临界资源为:{}",cs);  //得到的结果可能会不对,有时并不是0 

问题分析:

以上的结果可能是正数、负数、零。为什么呢?因为Java 中对变量的自增,自减并不是原子操作,必须从字节码来进行分析,++运算和--运算是先取出变量的值、运算得出结果、放进变量里,这三步操作才组成了++和--运算。

如上图:有时候线程2已经运算完了,但是还未把结果反馈回去,此时cpu的时间片用完了,被线程1抢走了,然后线程1拿到了脏数据,进行运算并反馈了结果,然后又轮到线程2继续执行, 把结果再反馈回去,这时候结果就是错误的了。

临界区cs

  • 在多个线程读共享资源没问题,当多个线程对共享资源读写操作时发生指令交错,就会出现线程安全问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
  • 为了避免临界区的竞态条件发生,有多种手段可以达到目的。

解决方案:

  • 阻塞式的解决方案: synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized

        synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意:

虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

private static Object lock=new Object();
//加同步代码块
 synchronized (lock) {
    cs++;//临界区
 }

解读:多个线程同时争夺这把锁,谁抢到了谁就能执行临界区里的代码,没抢到的线程会进入阻塞状态,抢到锁的线程如果执行完了临界区的代码才会释放锁,并唤醒因为没抢到锁而阻塞的线程,让他们继续抢锁。 

synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的不会被线程切换所打断。

synchronized代码块中指令是可以重排序的,只不过代码块之外是看不到它的重排序的。

语法

1.放在方法内

synchronized(对象) 
{
 临界区(对象需要受保护的代码)
}

2.放在方法上 

class Test{
public synchronized void test() {
		 临界区
	}
}
---等价于---
class  Test{
public void test() {
    synchronized(this){ //锁本类非静态对象
		 临界区
	}
}   

3.放在静态方法上

class Test{
public synchronized  static void test() {
		 临界区
	}
}
---等价于---
class  Test{
public void test() {
    //锁的是类对象
    synchronized(Test.class){ //静态对象不等于非静态对象,非静态对象存储在堆,静态的存储在方法区(是所有本类对象共享的)
		 临界区
	}
}    

线程八锁

就是对synchronized语法的8个练习题,我们来看看吧!分析出synchronized到底锁的是哪个对象

情况一:        

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    Number n = new Number();
    new Thread(()->{
        n.a();
    }).start();
    new Thread(()->{
        n.b();
    }).start();
}

static class Number{
    public synchronized void a(){
        System.out.print("a");
    }
    public synchronized void b(){
        System.out.print("b");
    }
}

答案为 ab 或者 ba,这两个线程锁住的都是n这个变量。

情况二:更情况一的不同之处就是在a方法里多了一个sleep

static class Number{
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print("a");
    }
    public synchronized void b(){
        System.out.print("b");
    }
}

 答案:如果第一个线程先抢到了锁那么,就会隔1s,再输出ab,如果第二个线程先抢到锁,那么先输出b再隔一秒输出a。这道题的特点就是告诉我们抢到锁的线程,如果睡眠了是不会释放锁的

 

情况三:多加了一个c方法,该方法是没有加synchronized的。

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    Number n = new Number();
    new Thread(()->{
        n.a();
    }).start();
    new Thread(()->{
        n.b();
    }).start();
    new Thread(()->{
        n.c();
    }).start();
}

static class Number{
    public synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print("a");
    }
    public synchronized void b(){
        System.out.print("b");
    }
    public void c(){
        System.out.print("c");
    }
}

结果:c 1s ab 或bc 1s a或cb 1s a,这个c是不受控制的,因为他不需要争抢锁,所以线程开启后抢到cpu时间片后就能立即执行。

情况四:线程一和线程二执行的是不同的对象

 public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            n1.a();
        }).start();
        new Thread(()->{
            n2.b();
        }).start();
    }

    static class Number{
        public synchronized void a() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("a");
        }
        public synchronized void b(){
            System.out.print("b");
        }
    }

结果为:b 1s后 a,因为成员方法上的synchronized锁的是this,所以a锁的是n1,b锁的是n2,所以都能立即得到锁。

情况五:方法a变为static的方法

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    Number n = new Number();
    new Thread(()->{
        n.a();
    }).start();
    new Thread(()->{
        n.b();
    }).start();
}

static class Number{
    public static synchronized void a() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print("a");
    }
    public synchronized void b(){
        System.out.print("b");
    }
}

答案:同情况四,因为a方法锁的是类对象,所以也不是同一把锁。

情况六:方法b也变为static的方法。

答案:1s后ab或者 b 1s后a,因为是同一把锁,互斥访问。

情况七

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{
            n1.a();
        }).start();
        new Thread(()->{
            n2.b();
        }).start();
    }

    static class Number{
        public static synchronized void a() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("a");
        }
        public synchronized void b(){
            System.out.print("b");
        }
    }

答案:也是锁的不是一个对象,所以是b1s后a

情况八:在情况七的方法b上加上static

答案:锁的是同一个对象,互斥访问。

变量的线程安全

线程三大问题:原子性,可见性,有序性

线程安全问题=共享数据+多线程+多线程修改共享数据

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的

    • 如果逃离了需要考虑线程安全

把局部变量引用的对象暴露给外部,可能就会不安全

int,float,double..这些属于基本数据类型,可以直接存放在栈帧的 局部变量中。而其他类对象,在局部变量表存放的是引用,实例在堆中

全局变量不安全例子:

俩线程同时写,只写成功一个,删除却要进行俩次

public class ThreadUnSafe {
    ArrayList<String> list = new ArrayList<>();//全局变量不安全。
    public void method1(){
        for (int i=0;i<200;i++) {
            method2(list);   //临界区,可以在这里加锁
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3( ArrayList<String> list) {
        list.remove(0);
    }
}
ThreadUnSafe t1 = new ThreadUnSafe();  
for (int i=0;i<200;i++) {
    new Thread(() -> {
        t1.method1();//同时引用同一个对象,共享list集合 所以线程不安全,可能同时add的时候只加了一个
    }).start();
}

局部变量不安全的例子:

如果把method2开一个新线程执行add操作,则存在线程安全性问题,因为虽然是局部变量,但是它的引用暴露给了外部。

public void method1(){
    ArrayList<String> list = new ArrayList<>();
       for (int i=0;i<200;i++) {
          method2(list);
          method3(list);
      }
  }
private void method2(ArrayList<String> list) {
    new Thread(()->{  //此时可能出现list集合被同时加和减的操作,会导致线程不安全
        list.add("1");
    }).start();
}
private void method3( ArrayList<String> list) {
        list.remove(0);
}

常见线程安全类

  • String
  • Integer,Boolean等包装类 没有synchronized但是被final 修饰了
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent(JUC)包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

  • 它们的单个方法是原子的,因为在方法上加了synchronized
  • 但注意它们多个方法的组合不是原子的

线程安全类方法的组合

如:不是安全的,key的值可能就被覆盖了

Hashtable hashtable = new Hashtable();
        //线程一、线程二
 if (hashtable.get("key") == null){
     hashtable.put("key",value);
 }

有锁会阻塞,但是线程2是在线程1执行完get时候,立马又去执行get的。get是一把锁,put是一把锁,get完后将锁释放线程2可以继续get

线程1执行完get之后,锁释放,此时有可能线程2抢占到了锁,所以线程2也判断get==null成立,这时线程2可以进入下面的逻辑。这样和我们希望的流程就有可能不同了。

不可变类线程安全性

String,Integer等都是不可变类,内部的状态不可以改变,因此它们的方法都是线程安全的、

或许有疑问,String有replace,substring等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }								//创建了一个新的对象
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

卖票问题

出线程安全性的代码如下:

public class TicketWindow {
    private int count;
    public int sell(int amount){
        //临界区
        if(this.count>=amount){
            this.count-=amount;//这里出问题,多个线程同时操作
            return  amount;
        }else return 0;
    }
    public TicketWindow(int count) {
        this.count = count;
    }
}
public static void main(String[] args) throws InterruptedException {
    TicketWindow window=new TicketWindow(10000);
    Vector<Integer> v=new Vector();//需要线程安全的集合
    List<Thread> threads=new ArrayList<>();//因为这个变量只是在main线程里
    for(int i=0;i<2000;i++){
        Thread thread = new Thread(() -> {
            int a = window.sell(2);//线程不安全
            v.add(a);//线程安全的,因为是Vector,如果是其他的则线程不安全,因为方法里多少会有读写的一些操作导致数据错误
        });
        threads.add(thread);
        thread.start();
    }
    //等待所有线程
    for (Thread thread : threads) {
        thread.join();
    }
    //卖出去的票和
    System.out.println(v.stream().mapToInt(i -> i).sum());//4000
    System.out.println(window.getCount());//6004
}

解决方法:在sell方法上加synchronized,保证临界区的原子性。

转账问题

public class Account {
    private int money;
    //转账操作
    public void transfer(Account target,int amount){
        if(this.money>=amount){
            this.setMoney(getMoney()-amount);
            target.setMoney(target.getMoney()+amount);
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Account a = new Account(2000);
    Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 400; i++) {
                a.transfer(b, 2);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 400; i++) {
                b.transfer(a, 3);
            }
        });
        t1.start(); t2.start();
        t2.join();  t1.join();
        System.out.println(a.getMoney()+" "+b.getMoney());//数目不对
}

如果只是在transfer方法上加synchronized的话,还是会出现问题。

因为synchronized只是锁this,而方法里涉及了两个不同的对象,同时只能a对象转账给b对象一笔账,同时只能b转a一笔,但是可能在同步代码块中,可能b要在读取自身money时,可能a恰巧想改变b的money,但是a的cpu时间片没了,b却读取了一个错误的值进行操作,操作完后,下一次a重新运行,又把上次没修改的值给修改了。

其实这里锁的是this的话,加了和没加一样,因为锁的是两个不同的对象,必须锁住两个线程唯一的对象,可以在方法上加static或者锁类,效率稍差。

下一篇synchronized的原理------------>synchronized原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值