【JavaSE】多线程之同步与死锁

产生原因:多个线程在同一时刻访问共享资源(临界区)

1.synchronized(内建锁)实现同步处理(加锁操作)

        在方法中使用synchronized(对象),一般可以锁定当前对象this,表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。

class MyThread implements Runnable{
    private int ticket = 1000;
    public void run(){
        for(int i=0; i<1000; i++){
            //同步代码块
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() +
                            "还剩下" + this.ticket-- + "票");
                }
            }
        }
    }
}
public class Test1{
    public static void main(String[] args) {
        MyThread mt= new MyThread();
        Thread th1 = new Thread(mt,"黄牛1");
        Thread th2 = new Thread(mt,"黄牛2");
        Thread th3 = new Thread(mt,"黄牛3");
        th1.start();
        th2.start();
        th3.start();
    }
}

        在方法声明上加synchronized,表示此时只有一个线程能够加入同步方法。

class MyThread implements Runnable{
    private int ticket = 100;
    public void run(){
        for(int i=0; i<100; i++){
            this.sale();
        }
    }
    public synchronized void sale(){
        if(this.ticket>0){    //还有票
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+
                    "还剩下"+this.ticket--+"票");
        }
    }
}
public class Test1{
    public static void main(String[] args) {
        MyThread mt= new MyThread();
        Thread th1 = new Thread(mt,"黄牛1");
        Thread th2 = new Thread(mt,"黄牛2");
        Thread th3 = new Thread(mt,"黄牛3");
        th1.start();
        th2.start();
        th3.start();
    }
}

2.synchronized对象锁概念(锁对象)

        synchronized(this)以及普通的synchronized方法,只能防止多个线程同时执行同一个对象的同步段。synchronized锁的是括号中的对象而非代码段。

例:观察synchronized锁多对象

class Sync{
    //锁的是this
    public synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"方法开始...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"方法结束");
    }
}
class MyThread implements Runnable{
    public void run(){
        Sync sync = new Sync();
        //sync,线程0
        //sync,线程1
        //sync,线程2
        //三个线程各有各的run方法,各锁各的sync,所以锁不住
        sync.test();
    }
}

public class Test1{
    public static void main(String[] args) {
        MyThread mt= new MyThread();
        for(int i=0; i<3; i++){
            new Thread(mt,"线程"+i).start();
        }
    }
}

通过上述代码以及运行结果,没有看到synchronized起到作用,三个线程同时运行test()方法。

例:锁同一个对象

class Sync{
    //锁的是this
    public synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"方法开始...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"方法结束");
    }
}
class MyThread implements Runnable{
    private Sync sync;
    public MyThread(Sync sync) {
        this.sync = sync;
    }
    public void run(){
        //现在只有一个sync所以一定可以锁得住
        sync.test();
    }
}

public class Test1{
    public static void main(String[] args) {
        Sync sync = new Sync();
        MyThread mt= new MyThread(sync);
        for(int i=0; i<3; i++){
            new Thread(mt,"线程"+i).start();
        }
    }
}

全局锁:锁代码段

I.使用类的静态同步方法

synchronized与static共同使用,此时锁的是类而非对象

class Sync{
    //此时锁的是下方代码段
    public static synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"方法开始...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"方法结束");
    }
}
class MyThread implements Runnable{
    public void run(){
        Sync sync = new Sync();
        sync.test();
    }
}

public class Test1{
    public static void main(String[] args) {
        MyThread mt= new MyThread();
        for(int i=0; i<3; i++){
            new Thread(mt,"线程"+i).start();
        }
    }
}

II.在代码块中锁当前Class对象

synchronized(类名称.class){}

class Sync{
    //此时锁的是下方代码段
    public void test(){
        synchronized (Sync.class){
            System.out.println(Thread.currentThread().getName()+"方法开始...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"方法结束");
        }
    }
}
class MyThread implements Runnable{
    public void run(){
        Sync sync = new Sync();
        sync.test();
    }
}

public class Test1{
    public static void main(String[] args) {
        MyThread mt= new MyThread();
        for(int i=0; i<3; i++){
            new Thread(mt,"线程"+i).start();
        }
    }
}

synchronized使用总结:

同步代码块:

I.synchronized(this){}

II.synchronized(类名称.class){} —类反射对象,全局锁

III.synchronized(任意对象){}

同步方法:

I.普通方法  //this

II.静态方法—全局锁     //类名.class

3.synchronized的底层实现

public class Test1{
    private static Object object = new Object();
    public static void main(String[] args)  {
        synchronized (object){
            System.out.println("hello world");
        }
    }
}

使用javap反编译后生成的部分字节码:

        执行同步代码块后首先要执行monitorenter指令,退出时执行monitorexit指令。使用synchronized实现同步,关键点就是要获取对象的监视器monitor对象。当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的monitor监视器。

        通常一个monitorenter指令会同时包含多个monitorexit指令。因为JVM要确保所获取的锁无论在正常执行路径或异常执行路径都能正确解锁。

同步方法底层实现:

public class Test1{
    public static void main(String[] args)  {
        foo();
    }
    public static synchronized void foo(){
        System.out.println("hello world");
    }
}

        当使用synchronized标记方法时,字节码会出现访问标记ACC_SYNCHPRONIZED。该标记表示在进入方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作。

        当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程置为当前线程,并且将monitor计数器+1。在目标对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁);否则需要等待,直到持有线程释放该锁。

        当执行monitorexit时,JVM需将锁对象计数器-1,当计数器减为0时,代表该锁已被释放掉,唤醒所有正在等待的线程去竞争该锁。

public class Test1{
    public static void main(String[] args)  {
        test();
        test1();
    }
    //当主线程进入test()方法后,已经拿到锁,monitor计数器加1,
    //要进入test1()方法时,此时monitor计数器的值不为0,要判断当前持有线程是否为访问线程,
    //如果是则进入,并且monitor计数器在加1
    public static synchronized void test(){
        System.out.println("hello world");
    }
    public static synchronized void test1(){
        System.out.println("hello world");
    }
}

        对象锁(monitor)机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的阻塞以及唤醒操作均需要操作系统由用户态切换到内核态,开销非常之大,因此效率很低。相比之下使用Java提供的Lock对象,性能更高一些。

        到了JDK1.6,对synchronized加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。所以在JDK1.6后synchronized的性能并不比Lock差。

4.CAS(Compare and Swap)

        锁的优化是将线程等待的时间变短。

        悲观锁:线程获取锁(JDK1.6之前内建锁)是一种悲观锁策略。假设每一次执行临界区代码(访问共享资源)都会产生冲突,所以当前线程获取到锁的同时也会阻塞其他未获取到锁的线程。

        乐观锁(CAS操作,无锁操作):假设所有线程访问共享资源时不会出现冲突,由于不会出现冲突自然就不会阻塞其他线程。因此线程就不会出现阻塞停顿的状态。出现冲突时,(既然是乐观锁,它就不会上锁)无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。(不断重试获取当前资源,一定是重试比阻塞获取到资源快)

4.1 CAS操作过程

CAS可以理解为CAS(V,O,N):

V:当前内存地址实际存放的值

O:预期值(旧值)

N:更新的新值

       当V=O时,期望值与内存实际值相等,该值没有被其他任何线程修改过,即值O就是目前最新的值,因此可以将新值N赋给V。反之,如果V!=O,表明该值已经被其他线程修改过了,因此O值并不是当前最新的值,返回V,无法修改。

                                          

        当多个线程使用CAS操作时,只有一个线程会成功,其余线程均失败,失败的线程会重新尝试(自旋)或挂起线程(阻塞)。

        内建锁在优化前最大的问题在于:在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,这是一种互斥同步(阻塞同步)。而CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起,也叫作非阻塞同步。

4.2 CAS的问题

4.2.1 ABA问题

ABA

{

线程1:  V -> A; N =1;

线程2:  V  = A; O= A;

线程3:  V -> B; N = 2;

线程4:  V -> A; N = 3;

}

        此时线程2不可以在修改值,要先从内存中获取当前的值,要不然线程3和线程4修改的值全部作废。

        解决方法:使用atomic包AtomicStampedReference类来解决

        添加版本号:1A -> 2B -> 3A

4.2.2自旋会浪费大量的CPU资源

        与线程阻塞相比,自旋会浪费大量的处理器资源。因为当前线程仍处于运行状态,只不过跑的是无用指令。例如:“踩刹车停车与熄火停车”

        解决:自适应自旋(重量级锁的优化):根据以往自旋等待时间能否获取锁,来动态调整自旋时间(其实就是调整循环次数)。如果在自旋时获取到锁,则会稍微增加下一次自旋的时间;否则就稍微减少下一次自旋的时长。

4.2.3公平性(处于自旋状态的锁比处于阻塞状态的锁更公平)

        内建锁无法实现公平机制(带速停车的起步一定比熄火停车的起步快),而Lock体系可以实现公平锁(等的时间长的先获取到锁)。

5.Java对象头

Java对象头 Mark Word 字段存放的内容:

对象的Hashcode

分代年龄

锁标记位

JDK1.6之后一共四种状态锁:

无锁  0   01

偏向锁  1  01

轻量级锁  00

重量级锁  10

        根据竞争状态的激烈程度,锁会自动进行升级,锁不能降级(为了提高锁获取与释放的效率)。

6.偏向锁(一个线程一个锁)

        偏向锁头部Epoch字段值:表示此对象偏向锁的撤销次数,默认撤销40次以上,表示此对象不再使用于偏向锁,当下次线程再次获取此对象时,直接变为轻量级锁。

        只有一次CAS操作,出现在第一次加锁时。

        大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低引入偏向锁。

        偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求一把锁。

偏向锁的获取:

        当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段存储锁偏向的进程ID,以后该进程在进入和退出同步块是不需要进行CAS操作(monitor机制)来加锁和解锁,直接进入。

        当线程访问同步块失败时,使用CAS竞争锁。并将偏向锁升级为轻量级锁。

偏向锁的撤销:开销较大

        偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)。

        如果持有线程已经终止,则将锁对象的对象头设置成为无锁状态。

        JDK6之后偏向锁默认开启。(两个存活的线程在同时竞争偏向锁,会把偏向锁升级成为轻量级锁)

                                                 

7.轻量级锁(多个线程在不同时间段获取锁)

        多个线程在不同的时间段请求同一把锁,也就是不存在锁竞争的情况。针对这种状况,JVM采用了轻量级锁来避免线程的阻塞与唤醒。

        尝试去将锁的指针指向当前线程,如果成功,当前线程获取到锁,如果失败,则不断进行CAS操作(自旋)

        锁只能降级不能升级(因为有多个线程竞争,降级后还是会膨胀,无意义)

                                             

8.三种锁特点:

a.偏向锁只会在第一次请求锁时采用CAS操作并将锁对象的标记字段记录下当前线程地址。在此后的运行过程中,持有偏向锁的线程无需加锁操作。针对的是锁仅会被同一线程持有的状况。

b.轻量级锁采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。针对的是多个线程在不同时间段申请同一把锁的情况。

c.重量级锁会阻塞、唤醒请求加锁的线程。针对的是多个线程同时竞争同一把锁的情况。JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。

9.其他优化

锁粗化:

        锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁。

例:所有线程只有一个sb,存在线程安全问题

public class Test1{
    private static StringBuffer sb = new StringBuffer();
    public static void main(String[] args()){
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

        这里每次调用StringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁解除:

        删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则认为此代码时线程安全的,无需加锁。

例:每个线程都存在自己的sb,不存在线程安全问题,去掉方法中的synchronized

public class Test1{
    public static void main(String[] args){
        StringBuffer sb = new StringBuffer();
        sb.append("a").append("b").append("c");
    }
}

        虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以这过程是线程安全的,可以将锁消除。

10.死锁

        同步的本质在于:一个线程等待另一个线程执行完毕后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁。

例:

class Pen{
    private Pen pen;
    public Pen getPen() {
        return pen;
    }
}

class Book{
    private Book book;
    public Book getBook() {
        return book;
    }
}

public class Test2{
    private static Pen pen = new Pen();
    private static Book book = new Book();
    public static void main(String[] args) {
        new Test2().test();
    }
    public void test(){
        Thread thread1 = new Thread(()->{
            synchronized (pen){
                System.out.println("我有笔,把你的本子给我");
                synchronized (book){
                    System.out.println("把你的本子给我");
                }
            }
        });

        Thread thread2 = new Thread(()->{
            synchronized (book){
                System.out.println("我有本子,把你的笔给我");
                synchronized (pen){
                    System.out.println("把你的笔给我");
                }
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

死锁一旦出现之后,整个程序就将中断执行。过多的同步会造成死锁,对于资源的上锁一定要注意不要成“环”。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gx1500291

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值