线程同步与死锁(synchronized关键字详解)

线程同步与死锁(synchronized关键字详解)

文章目录

  • 线程同步与死锁(synchronized关键字详解)
      • 1.synchronized(关键字)实现同步处理
        • 1.同步代码块
        • 2.同步方法
        • 3.总结
      • 2.synchronized的实现原理
      • 3. synchronized的各种性能(⭐)
      • 4.synchronized关键字锁多对象的案例
        • 改进一:让锁住同一个对象
        • 改进二:利用全局锁(即让synchronized锁这个类对应的Class对象)
      • 5.同步关键字:synchronized的优化(⭐)
        • 1.CAS
          • 1.什么是CAS?
          • 3.CAS的问题
        • 2.锁的优化升级
        • 3.总结
      • 6.死锁:(⭐)
    • ThreadLocal
        • 1.概念
        • 2.ThreadLocal的内部实现

1.synchronized(关键字)实现同步处理

这是为解决“非线程安全”问题,当多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题;

概念:所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来

为甚麽需要同步处理呢?
假如我们设计一个卖票的程序(run方法中模拟延时用sleep),用三个多线程同时来卖,当我们执行后,剩余票数会出现负的情况,这是因为这几个线程都毫无规律的同时抢占资源来卖票,在最后一轮不免有多个线程进入卖票,导致为负;

同部处理需要关键字synchronized来实现 ;

根据synchronized关键字修饰不同代码,可划分为如下:

分类 具体分类 被锁的对象 伪代码
方法 实例方法 类的实例对象 public synchronized void fun(){}
方法 静态方法 类对象 public static synchronized void fun(){}
代码块 实例对象 类的实例对象 synchronized (this){}
代码块 class对象 类对象 synochronized(SynochronizedDemo.class){}
代码块 任意实例对象Object 实例对象Object String s = “ss”;
synochronized(s){}

1.同步代码块

这种方式是在方法里拦截的,也就是说进入到方法中的线程依然可能会有多个。

下面看一个卖票的例子:

class MyThreadTick extends Thread {
    @Override
    public void run() {
        int tick = 10;
        while(tick > 0) {
            System.out.println("剩余票数:"+(tick--));
        }
    }
}

class MyRunnableTick implements Runnable {
    private int tick = 20;
    @Override
    public void run() {
        //延时代码必须在这,如果放在synchronized代码块内,则第一个线程进入后,其他线程就没有机会进入
        //了;
        while(tick>0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (this){
                if(tick > 0) {
                    System.out.println(Thread.currentThread().getName()+"进行了销售,剩余票数:"+ --tick);
                    if(tick == 0) {
                        System.out.println("票已经卖完了!!!!");
                    }
                }
            }

        }
    }
}
public class TestMyRunnableTick {
    public static void main(String[] args) {

        Runnable myRunnableTick = new MyRunnableTick();
        new Thread(myRunnableTick,"销售A").start();
        new Thread(myRunnableTick,"销售B").start();
        new Thread(myRunnableTick,"销售C").start();
    }
}

输出:
销售B进行了销售,剩余票数:19
销售A进行了销售,剩余票数:18
销售C进行了销售,剩余票数:17
销售A进行了销售,剩余票数:16
销售B进行了销售,剩余票数:15
销售C进行了销售,剩余票数:14
销售A进行了销售,剩余票数:13
销售B进行了销售,剩余票数:12
销售C进行了销售,剩余票数:11
销售B进行了销售,剩余票数:10
销售A进行了销售,剩余票数:9
销售C进行了销售,剩余票数:8
销售B进行了销售,剩余票数:7
销售A进行了销售,剩余票数:6
销售C进行了销售,剩余票数:5
销售B进行了销售,剩余票数:4
销售C进行了销售,剩余票数:3
销售A进行了销售,剩余票数:2
销售B进行了销售,剩余票数:1
销售C进行了销售,剩余票数:0
票已经卖完了!!!!

2.同步方法

class MyRunnableTick implements Runnable {
    private int tick = 20;
    @Override
    public void run() {
        while(tick>0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sale();
        }
    }
    public synchronized void sale() {
        if(tick > 0) {
            System.out.println(Thread.currentThread().getName()+"进行了销售,剩余票数:"+(--tick));
            if(tick == 0) {
                System.out.println("票已经销售完了!!!!");
            }
        }
    }
}
public class TestMyRunnableTick {
    public static void main(String[] args) {

        Runnable myRunnableTick = new MyRunnableTick();
        new Thread(myRunnableTick,"销售A").start();
        new Thread(myRunnableTick,"销售B").start();
        new Thread(myRunnableTick,"销售C").start();
    }
}

销售A进行了销售,剩余票数:19
销售B进行了销售,剩余票数:18
销售C进行了销售,剩余票数:17
销售C进行了销售,剩余票数:16
销售B进行了销售,剩余票数:15
销售A进行了销售,剩余票数:14
销售C进行了销售,剩余票数:13
销售B进行了销售,剩余票数:12
销售A进行了销售,剩余票数:11
销售C进行了销售,剩余票数:10
销售A进行了销售,剩余票数:9
销售B进行了销售,剩余票数:8
销售C进行了销售,剩余票数:7
销售A进行了销售,剩余票数:6
销售B进行了销售,剩余票数:5
销售B进行了销售,剩余票数:4
销售A进行了销售,剩余票数:3
销售C进行了销售,剩余票数:2
销售C进行了销售,剩余票数:1
销售A进行了销售,剩余票数:0
票已经销售完了!!!!

3.总结

同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度会很慢。

2.synchronized的实现原理

在jdk1.6之前,synchronized是一个重量级锁;我们都知道,在Object中维护了一个监视器Monitor,synchronized正是通过这个监视器来实现同步的,监视器锁的本质是依赖于底层的操作系统mutex lock(互斥锁)来实现的,每个对象都对应一个可称为互斥锁的标记,这个标记用来保证在任意时刻只能有一个线程访问该锁;

老版本jdk的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

3. synchronized的各种性能(⭐)

  • 关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到对象锁的;这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的;
  • 当一个线程执行的代码出现异常时,其所持有的锁会自动释放;
  • 同步不能继承

4.synchronized关键字锁多对象的案例

class Sync {
    public synchronized void test() {
        System.out.println(Thread.currentThread().getName()+": test方法开始执行");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+": test方法结束");
    }
}
class Mythread extends Thread {
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestMyRunnableTick {
    public static void main(String[] args) {
        for(int i = 0; i<3; i++) {
            Mythread mythread = new Mythread();
            mythread.start();
        }
    }
}

输出:
Thread-2: test方法开始执行
Thread-0: test方法开始执行
Thread-1: test方法开始执行
Thread-1: test方法结束
Thread-0: test方法结束
Thread-2: test方法结束

前三行几乎同时输出,后三行在一秒后同时输出,这说明了这三个线程同时进入了被synchronized修饰的方法内,可见并没有达到锁住多个线程进入这个方法的目的,这是为甚麽呢?
原因:
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。

改进一:让锁住同一个对象

要想达到我们的目的,我们可以将上面代码改变一下,让锁住同一个对象

class Sync {
    public  void test() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+": test方法开始执行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": test方法结束");
        }
        }
}
class Mythread extends Thread {
    private Sync sync;
    public Mythread(Sync sync) {
        this.sync = sync;
    }
    @Override
    public void run() {
        sync.test();
    }
}

public class TestMyRunnableTick {
    public static void main(String[] args) {
        Sync sync = new Sync();
        for(int i = 0; i<3; i++) {
            Mythread mythread = new Mythread(sync);
            mythread.start();
        }
    }
}

输出:
Thread-0: test方法开始执行
Thread-0: test方法结束
Thread-2: test方法开始执行
Thread-2: test方法结束
Thread-1: test方法开始执行
Thread-1: test方法结束

改进二:利用全局锁(即让synchronized锁这个类对应的Class对象)

  • 为甚麽锁住这个类的Class对象就行了呢,我们来回顾一下反射:

    在JVM中任何一个类都有一个唯一的Class对象 ,此对象记录该类的组成结构,通过该class对象,可以反向查找到这个类的信息,称之为反射;

class Sync {
    public void test() {
        synchronized (this.getClass()){   //通过对象获取Class对象,getClass()方法是Object类中的;
            System.out.println(Thread.currentThread().getName()+": test方法开始执行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+": test方法结束");
        }
        }
}
class Mythread extends Thread {
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestMyRunnableTick {
    public static void main(String[] args) {
        for(int i = 0; i<3; i++) {
            Mythread mythread = new Mythread();
            mythread.start();
        }
    }
}

5.同步关键字:synchronized的优化(⭐)

1.CAS

1.什么是CAS?

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。 因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

#####2.CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。
老版jdk的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

3.CAS的问题
  • ABA问题 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。 (⭐)在JDK1.5后的atomic包中提供了 AtomicStampedReference来解决ABA问题,解决思路就是这样的。
  • 自旋会浪费大量的处理器资源与线程阻塞相比,自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。

我们可以用等红绿灯作为例子。Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如我们在同步代码块中只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更合适。

然而,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞。JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)。
就我们的例子来说,如果之前不熄火等待了绿灯,那么这次不熄火的时间就长一点;如果之前不熄火没等待绿灯,那么这次不熄火的时间就短一点。
公平性
自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
内建锁无法实现公平机制,而lock体系可以实现公平锁

2.锁的优化升级

多线程同一时间访问同一个资源(锁),产生竞争,线程阻塞和唤醒,这将会带来很大的效率问题!

JVM实现,开发者经过大量的程序分析,多线程访问同一资源的时候,大多数情况下并不是同一时间进行的。

所以有了优化:

  • 无锁: 无同步不使用synchronized;

  • 偏向锁:一个线程访问一个资源,产生竞争(多线程)

  • 轻量级锁:多线程不同时间访问同一资源

  • 重量锁:多线程同一时间访问同一资源

这三种都使用了synchronized关键字,但具体是哪一种锁,我们也不知道,这是由JVM决定的,这是JVM对synchronized的优化;

锁的升级(膨胀):无锁 —> 偏向锁 ----> 轻量级锁 ----> 重量级锁

3.总结

Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。

  1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自
    旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
  2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象
    原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程
    中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况

其他优化: (开发者的优化)

锁的粗化:将连续的加锁解锁变成更大范围的加锁解锁(人为的优化,写代码的优化层次)

锁消除:即删除不必要的锁;

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

StringBuff属于线程安全,每次执行appand方法就会加锁,完毕时会解锁,这样会让执行效率大大降低,而且,根本就不会有多线程来同时访问StringBuff对象,所以没有必要加锁,所以使用StringBuilder,其实一般都是使用StringBuilder;

6.死锁:(⭐)

死锁可以用下面这张图来表达:

线程同步与死锁(synchronized关键字详解)_第1张图片

设计一个死锁:

class Pen {
    private String pen = "pen";
    public String getPen() {
        return pen;
    }
}
class Book {
    private String book = "book";

    public String getBook() {
        return book;
    }
}
public class TestLockClear {
    private static Pen pen = new Pen();
    private static Book book = new Book();
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {      //匿名内部类
            @Override
            public void run() {
                synchronized(pen) {
                    System.out.println("i have pen ,but not have book");
                }
                synchronized (book) {   //这个synchronized一定得包含在上面那个之内才能达到死锁的效果
                    System.out.println("i hava pen and book");
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(book) {
                    System.out.println("i have book ,but not have pen");
                }
                synchronized (pen) {
                    System.out.println("i hava pen and book");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}

有一定概率出现死锁:(如下结果,一直停止不了)

book: i have book ,but not have pen
pen: i have pen ,but not have book

ThreadLocal

1.概念

在set方法中,每个线程都会新建自己的map,所以不同线程拥有自己的独立的map;
ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说ThreadLocal 可以为每个线程创建一个单独的变量副本,相当于线程的 private static 类型变量。
ThreadLocal 的作用和同步机制有些相反:同步机制是为了保证多线程环境下数据的一致性;而 ThreadLocal 是保证了多线程环境下数据的独立性。

下面看它的简单应用:

public class TestThreadLocal {
    private static ThreadLocal<String> threadLocal = new ThreadLocal();
    private String commonString = "thread---A";
    public static void main(String[] args) {
        threadLocal.set("alla");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("thread----子线程");
                System.out.println(threadLocal.get());  //thread----子线程
            }
        },"子线程");
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(threadLocal.get());
    }
}

结果:

thread----子线程
alla

从运行结果可以看出,对于 ThreadLocal 类型的变量,在一个线程中设置值,不影响其在其它线程中的值。也就是说ThreadLocal 类型的变量的值在每个线程中是独立的。

2.ThreadLocal的内部实现

先看看set方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • set(T value) 方法中,首先获取当前线程,然后在获取到当前线程的 ThreadLocalMap,如果 ThreadLocalMap 不为null,则将 value 保存到 ThreadLocalMap 中,并用当前 ThreadLocal 作为 key;否则创建一ThreadLocalMap 并给到当前线程,然后保存 value。
  • ThreadLocalMap 相当于一个 HashMap,是真正保存值的地方;
  • ThreadLocalMap是我们要关注的核心,从上面的讲解和它的名字可以猜想到这是每个线程所独立维护的一个哈希表,这个哈希表从源码中我们可以得知它是一个链表数组(private Entry[] table) ,这跟我们之前的hashMap差不多的,它的添加数据等等都是差不多的实现,只不过这里的键值对中的键是以当前线程的ThreadLocal对象;
  • Entry 用于保存一个键值对,其中 key 以弱引用的方式保存;

关于引用有下面这些类型:

先做了解:

  • 强引用:

    example:String str = “hello”;

  • 软引用

  • 弱引用:

  • 幻引用(幽灵引用)

你可能感兴趣的:(Java进阶)