synchronized详解

线程同步

首先看一个例子

class Shop implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"还有" + ticket-- + "张票");
        }
    }
}

public class Test03 {
    public static void main(String[] args) {
        Shop shop = new Shop();
        new Thread(shop,"A").start();
        new Thread(shop,"B").start();
        new Thread(shop,"C").start();
    }
}

结果出现了票为负数的情况
结果可见票居然出现了负数,这种问题称之为不同步。
synchronized解决的是线程之间同步问题,所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
使用synchronized有两种方法:

  • 同步代码块
  • 同步方法

同步代码块代码实现

class Shop implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        for (int i=0;i<10;i++) {
            synchronized (this) {
                if(ticket >=0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"还有" + ticket-- + "张票");
                }
            }
        }
    }
}

public class Test03 {
    public static void main(String[] args) {
        Shop shop = new Shop();
        new Thread(shop,"A").start();
        new Thread(shop,"B").start();
        new Thread(shop,"C").start();
    }
} 

在这里插入图片描述
使用同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}
需要被同步的代码:操作共享数据(该例为ticket),不能多也不能少。
同步监视器:俗称“锁”,任何一个类的对象都可用作锁,要求多个线程用同一把锁。
在实现Runable接口实现多线程,建议使用当前对象this作为同步监视器。
在继承Thread类实现多线程,建议使用当前类作为同步监视器。
同步方法代码实现

class Shop implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.sell();
        }
    }
    synchronized void sell() {
        if (ticket >= 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "还有" + ticket-- + "张票");
        }
    }
}

public class Test03 {
    public static void main(String[] args) {
        Shop shop = new Shop();
        new Thread(shop, "A").start();
        new Thread(shop, "B").start();
        new Thread(shop, "C").start();
    }
}

同步方法:当操作共享数据的完整代码在一个方法中时,给此方法声明为同步。
synchronized void sell() {}
对于同步方法我们依然不能忽略同步监视器,只是不需要我们显式声明,当用于普通方法时同步监视器为当前类对象,当用于静态方法时则为当前类本身。

死锁

先看一个例子

public class Test {
    public static void main(String[] args) {
        Test test1=new Test();
        Test test2=new Test();
        new Thread(()->{
            synchronized (test1) {
                test1.show();
                synchronized (test2) {
                    test2.show();
                }
            }
        }).start();
        new Thread(()->{
            synchronized (test2) {
                test2.show();
                synchronized (test1) {
                    test1.show();
                }
            }
        }).start();
    }
  public  void show(){
        System.out.println("我是A");
    }
}

在这里插入图片描述
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
死锁的解决办法:资源上锁不要成环,不要过多的使用同步

synchronized实现原理

对象锁(monitor)机制
对于每个同步监视器,在执行同步代码块以后首先执行monitorenter指令获得同步监视器的monitor,然后程序向下执行,这一步骤是互斥的,同一时间只能有一个线程获取monitor,当线程获取monitor后,同步监视器的计数器加1,表示当前同步监视器已经有线程获取。退出的时候执行monitorexit指令,同步监视器的计数器减一直到减为0,表示该同步监视器被释放,这种计数器的好处是允许同一线程重复获取同一把锁,称为锁的可重入性。
可重入性代码:

class Reentry {
    synchronized void A(){
        System.out.println("我是A");
        this.B();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    synchronized void B(){
        System.out.println("我是B");
    }
}
public class Test {
    public static void main(String[] args) {
        Reentry reentry=new Reentry();
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                reentry.B();
            }
        });
        thread.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                reentry.A();
            }
        }).start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized优化

线程获取锁是一种”悲观锁“策略,假设每一次线程获取琐时都会遇到竞争,并且当该线程获得锁后其他竞争线程会阻塞。而CAS则是一种”乐观锁“策略,它线程访问共享资源不会遇到竞争,当然也不会阻塞其他线程的工作。
CAS操作过程:
CAS又叫做比较交换来鉴别线程是否出现冲突,当出现冲突一直重试当前操作直到没有冲突为止。
CAS(V,O,N)包含三个值,V当前内存地址存放的实际值,O预期值,N需要更改得新值。
交换过程:线程从内存中读入数据,当V值与O值相同时说明该值为最新值则把新值赋给V,当V值与O值不相同时,说明该值已经被其他线程修改所以无法把N值赋给V,此时只需要直接返回V值。并且会再次尝试交换。
CAS可能出现的问题:
ABA问题:在这里插入图片描述
旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题。
自旋问题:
线程CAS失败会进行自旋,自旋会浪费大量的处理器资源,对此JVM给出的解决方案为”自适应自旋“,解决思路为,本次的自旋次数与上次CAS成功的次数做比较,从而增加或者减少本次自旋的时间。
偏向锁
当任一时刻只有同一个线程获取锁,偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程
中,持有该偏向锁的线程的加锁操作将直接返回。
轻量级锁
不同时刻只有某一个线程获取锁, 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象
原本的标记字段。
重量级锁
同一时刻有多个线程获取同一把锁, 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值