java同步锁-详细易懂

目录

同步锁的引入:

无同步:

输出结果:

加同步锁:

输出结果:

解决办法:

方法一:

方法二:

输出结果:

同步锁机制:

同步锁机制:

synchronized的锁是什么?

注意:

同步的范围:

1、代码是否存在线程安全

2、如何解决

切记:

锁的释放:

释放锁的操作:

不会释放锁的操作:

单例模式-懒汉式-双重加锁校验:

第一次判断singleton是否为null

第二次判断singleton是否为null

线程的死锁问题:

死锁:

产生死锁的四个必要条件

解决方法:

Lock锁:

synchronized 与 Lock 的对比:


同步锁的引入:

java中cpu分给每个线程的时间片是随机的并且在java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源。如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。比如下面程序:

无同步:

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        thread1.setName("线程一");
        thread2.setName("线程二");
        thread3.setName("线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread extends Thread{
    static int  tick = 10;
    
    public MyThread() {
    }

    @Override
    public void run() {
        while (true) {
            if (tick > 0) {
                System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
            }
        }
    }
}

输出结果:

线程一 剩余tick:9

线程一 剩余tick:9

线程一 剩余tick:5

线程一 剩余tick:4

线程一 剩余tick:3

线程二 剩余tick:7

线程二 剩余tick:6

线程三 剩余tick:8

线程二 剩余tick:2

线程一 剩余tick:1

可以看到票数不是按照顺序减少,票刚开始卖时,有两个线程同时进入临界区,导致票买了两张,总数却只减少了一张。

加同步锁:

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        thread1.setName("线程一");
        thread2.setName("线程二");
        thread3.setName("线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread extends Thread{
    static int  tick = 10;

    public MyThread() {
    }

    @Override
    public synchronized void run() {
        while (true) {
            if (tick > 0) {
                System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
            }
        }
    }
}

输出结果:

线程一 剩余tick:9

线程一 剩余tick:6

线程一 剩余tick:5

线程一 剩余tick:4

线程一 剩余tick:3

线程二 剩余tick:8

线程二 剩余tick:1

线程三 剩余tick:7

线程二 剩余tick:0

线程一 剩余tick:2

这时一种非常典型的同步锁错误,以为自己加了锁,其实锁加在了各自对象的非静态方法上,即对同一个对象,这把锁才有用,我们创建了三个不同的对象,三个有各自的方法,此时锁方法无法锁住任何临界区。

解决办法:

  1. 让MyThread实现Runnable接口,只创建一个对象,将同一个对象作为参数传给Thread的构造器。
  2. 在run方法中用MyThread.class锁住代码块。

方法一:

public class Main {
    public static void main(String[] args) {
        MyThread mt = new MyThread();//只创建一个MyThread对象
        Thread thread1 = new Thread(mt);
        Thread thread2 = new Thread(mt);
        Thread thread3 = new Thread(mt);
        thread1.setName("线程一");
        thread2.setName("线程二");
        thread3.setName("线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread implements Runnable{//实现Runnable接口
    static int  tick = 10;

    public MyThread() {
    }

    @Override
    public synchronized void run() {
        while (true) {
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
                }
        }
    }
}

方法二:

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        thread1.setName("线程一");
        thread2.setName("线程二");
        thread3.setName("线程三");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyThread extends Thread{
    static int  tick = 10;

    public MyThread() {
    }

    @Override
    public  void run() {
        while (true) {
            synchronized (this.getClass()) {
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName()+" 剩余tick:"+--tick);
                }
            }
        }
    }
}

输出结果:

线程一 剩余tick:9

线程一 剩余tick:8

线程一 剩余tick:7

线程一 剩余tick:6

线程一 剩余tick:5

线程一 剩余tick:4

线程一 剩余tick:3

线程一 剩余tick:2

线程一 剩余tick:1

线程一 剩余tick:0

如此这般,票数才能按照顺序减少,而不会一个消费者看到窗口还有八张票,另一个消费者看到还有2张票。也不会出现两个消费者同时买到同一张票

同步锁机制:

同步锁机制:

同步机制中的锁在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

同步方法的锁:静态方法(类名.class)、非静态方法(this)

同步代码块:自己指定,很多时候也是指定为this或类名.class

注意:

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全

一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

同步的范围:

1、代码是否存在线程安全

(1)明确哪些代码是多线程运行的代码,如窗口

(2)明确多个线程是否有共享数据,如车票

(3)明确多线程运行代码中是否有多条语句操作共享数据,run方法

2、如何解决

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

切记:

范围太小:没锁住所有有安全问题的代码

范围太大:没发挥多线程的功能。

锁的释放:

释放锁的操作:

  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作:

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,陈果老师说过,带锁睡眠是多线程编程的大忌
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
  3. 应尽量避免使用suspend()和resume()来控制线程

单例模式-懒汉式-双重加锁校验:

class Singleton {
    private static Singleton instance = null;
    private Singleton(){//私有化构造方法}
    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance=new Singleton();
                } 
            } 
        }
        return instance;
    } 
}

第一次判断singleton是否为null

  第一次判断是在Synchronized同步代码块外进行判断,由于单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以,第一次判断,是为了在singleton对象已经创建的情况下,避免进入同步代码块,提升效率。

第二次判断singleton是否为null

第二次判断是为了避免以下情况的发生。

  1. 假设:线程A已经经过第一次判断,判断singleton=null,准备进入同步代码块.
  2. 此时线程B获得时间片,犹豫线程A并没有创建实例,所以,判断singleton仍然=null,所以线程B创建了实例singleton。
  3. 此时,线程A再次获得时间片,犹豫刚刚经过第一次判断singleton=null(不会重复判断),进入同步代码块,这个时候,我们如果不加入第二次判断的话,那么线程A又会创造一个实例singleton,就不满足我们的单例模式的要求,所以第二次判断是很有必要的。

线程的死锁问题:

final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
new Thread() {//匿名内部类
    @Override
    public void run() {
        synchronized (s1) {
            s2.append("A");
            synchronized (s2) {
                s2.append("B");
                System.out.print(s1);
                System.out.print(s2);
            }
        }
    }
}.start();
new Thread() {//匿名内部类
    @Override
    public void run() {
        synchronized (s2) {
            s2.append("C");
            synchronized (s1) {
                s1.append("D");
                System.out.print(s2);
                System.out.print(s1);
            }
        }
    }
}.start();

死锁:

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

产生死锁的四个必要条件

1.互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。

2.请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。

3.不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。

4.循环等待:发生死锁时,线程进入死循环,永久阻塞。

解决方法:

专门的算法、原则

尽量减少同步资源的定义

尽量避免嵌套同步

Lock锁:

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

class A{
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码;
        }
        finally{//注意:如果同步代码有异常,要将unlock()写入finally语句块
            lock.unlock();
        }
    }
}

synchronized 与 Lock 的对比:

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)所以优先使用顺序:Lock -> 同步代码块->同步方法

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值