Java并发编程:内置锁 Synchronized

    在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程访问某一共享、可变数据时,始终都不会导致数据破坏以及其他不该出现的结果。而所有的并发模式在解决这个问题时,采用的方案都是序列化访问临界资源 。在 Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。本文针对 synchronized 内置锁 详细讨论了其在 Java 并发 中的应用,包括它的具体使用场景(同步方法、同步代码块、实例对象锁 和 Class 对象锁)、可重入性 和 注意事项。synchronized 使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果。这种机制常常称为互斥量(mute)

一. 线程安全问题

在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个 共享、可变资源 的情况,这种资源可以是:一个变量、一个对象、一个文件等。特别注意两点,

  • 共享: 意味着该资源可以由多个线程同时访问;
  • 可变: 意味着该资源可以在其生命周期内被修改。

     所以,当多个线程同时访问这种资源的时候,就会存在一个问题:

     由于每个线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。 

现在是有四个线程在买票,当num=1的时候线程1,线程2,线程3,线程4都进来了,此时线程1,2,3,4的num=1都抢到cpu的执行时间片了,现在可能出现线程1的输出Thread-0:1,线程2此时输出Thread-1:0,线程3输出Thread-2:-1;此时就出现了不安全的问题了;
package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  20:58 2018/5/31
 * @Description: 用买票的案例说明线程的安全问题
 */
class Ticket implements Runnable {

    private  int num=100;
    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"......sale....."+num--);
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1=new Thread(ticket);
        Thread t2=new Thread(ticket);
        Thread t3=new Thread(ticket);
        Thread t4=new Thread(ticket);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

输出的结果最后的几条数据是:

Thread-0--sale---3
Thread-3--sale---2
Thread-2--sale---1
Thread-1--sale---0
Thread-0--sale----1
Thread-3--sale----2

Process finished with exit code 1


2. 线程安全问题的原因

    这其实就是一个线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。这里面,这个资源被称为:临界资源。也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。

  不过,当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。

    1:多个线程在操作共享的数据

    2:操作共享的线程代码有多条

3. 线程安全问题的解决方式

实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源 。即在同一时刻,只能有一个线程访问临界资源,也称作 同步互斥访问。换句话说,就是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

  在 Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。本文主要讲述 synchronized 的使用方法

4. synchronized 同步方法或者同步块

        在了解 synchronized 关键字的使用方法之前,我们先来看一个概念:互斥锁,即 能到达到互斥访问目的的锁。举个简单的例子,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。

  在 Java 中,可以使用 synchronized 关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

同步代码块的格式:

synchronized(对象){
     需要被同步的代码;
}
package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  20:58 2018/5/31
 * @Description: 用买票的案例说明线程的安全问题
 */
class Ticket implements Runnable {

    private int num = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "......sale....." + num--);
                }
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        Thread t4 = new Thread(ticket);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

当在某个线程中执行这段代码块,该线程会获取对象lock的锁,从而使得其他线程无法同时访问该代码块。其中,lock 可以是 this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。特别地, 实例同步方法 与 synchronized(this)同步块 是互斥的,因为它们锁的是同一个对象。但与 synchronized(非this)同步块 是异步的,因为它们锁的是不同对象。 

synchronized方法

package com.huanghe.chapter21;

import sun.invoke.util.BytecodeName;

/**
 * @Author: River
 * @Date:Created in  22:09 2018/5/31
 * @Description:
 */
public class BankDemo {
    public static void main(String[] args) {
        Custom c = new Custom();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

class Bank{
    private int sum;

    //这个方法会引起线程不安全问题,比如线程1进来执行了sum=0+100=100;之后切换到了线程2进行执行sum=sum+100=200,线程2执行之后
    //输出的是200,线程2执行之后切换到线程1输出200,所以会输出200,200,这就出现问题了,此时在方法出添加synchronized,就可以避免
    public synchronized void add(int num) {
        sum = sum + num;
        System.out.println("sum="+sum);
    }
}

class Custom implements Runnable {
    private Bank b = new Bank();

    @Override
    public void run() {
        for (int i = 0; i <3 ; i++) {
            b.add(100);
        }
    }
}

不过需要注意以下三点:

  1)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程不能访问该对象的其他 synchronized 方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

  2)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程能访问该对象的非 synchronized 方法。这个原因很简单,访问非 synchronized 方法不需要获得该对象的锁,假如一个方法没用 synchronized 关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的。

  3)如果一个线程 A 需要访问对象 object1 的 synchronized 方法 fun1,另外一个线程 B 需要访问对象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

验证同步代码块使用的是哪个锁?

package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  9:32 2018/6/1
 * @Description:
 */
public class SynFunctionLockDemo {
    public static void main(String[] args) {
        Ticket1 t = new Ticket1();
        System.out.println(t);

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        //让主线程sleep
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag=false;
        t2.start();
    }
}

class Ticket1 implements Runnable {

    private int num = 100;

    Object obj = new Object();

    boolean flag = true;


    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (this) {
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "......obj....." + num--);
                    }
                }
            }
        } else {
            while (true) {
                show();
            }
        }
    }

    public synchronized void show() {
        if (num > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "......fun....." + num--);
        }
    }
}

结果:

Thread-0......obj.....100
Thread-0......obj.....99
Thread-0......obj.....98
Thread-1......fun.....97
Thread-1......fun.....96
Thread-1......fun.....95
Thread-1......fun.....94
Thread-1......fun.....93
Thread-1......fun.....92
Thread-1......fun.....91
Thread-1......fun.....90
Thread-1......fun.....89
Thread-1......fun.....88
Thread-1......fun.....87
Thread-1......fun.....86
Thread-1......fun.....85
Thread-1......fun.....84
Thread-1......fun.....83
Thread-1......fun.....82
Thread-1......fun.....81
Thread-1......fun.....80
Thread-1......fun.....79
Thread-1......fun.....78
Thread-1......fun.....77
Thread-1......fun.....76
Thread-1......fun.....75
Thread-1......fun.....74
Thread-1......fun.....73
Thread-1......fun.....72
Thread-1......fun.....71
Thread-1......fun.....70
Thread-1......fun.....69

可以验证同步函数使用的锁是this

同步函数和同步代码块的区别:

1:同步方法使用synchronized修饰方法,在调用该方法前,需要获得内置锁(java每个对象都有一个内置锁),否则就处于阻塞状态

2:同步代码块使用synchronized(object){}进行修饰,在调用该代码块时,需要获得内置锁,否则就处于阻塞状态

3:同步函数使用的锁匙this,而同步代码块使用的锁匙任意的对象

静态同步函数使用的锁(class 对象锁,类.class):

特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是类锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}

class InsertData { 

    // 非 static synchronized 方法
    public synchronized void insert(){
        System.out.println("执行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行insert完毕");
    }

    // static synchronized 方法
    public synchronized static void insert1() {
        System.out.println("执行insert1");
        System.out.println("执行insert1完毕");
    }
}/* Output: 
        执行insert
        执行insert1
        执行insert1完毕
        执行insert完毕
 *///:~

根据执行结果,我们可以看到第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。下面,我们看一下 synchronized 关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:

有一点要注意:对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

四. 可重入性

一旦有一个线程访问某个对象的synchronized修饰的方法或代码区域时,该线程则获取这个对象的锁,其他线程不能再调用该对象被synchronized影响的任何方法。那么,如果这个线程自己调用该对象的其他synchronized方法,Java是如何判定的?这就涉及到了Java中锁的重要特性:可重入性,

重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

public class Father  
{  
    public synchronized void doSomething(){  
        ......  
    }  
}  
  
public class Child extends Father  
{  
    public synchronized void doSomething(){  
        ......  
        super.doSomething();  
    }  
}  

 子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。

由于Father和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。

    同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。

五. 死锁

常见的情景之一是同步的嵌套

package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  10:49 2018/6/1
 * @Description:
 */
public class DeadLockTest {
    public static void main(String[] args) {
        Test a = new Test(true);
        Test b = new Test(false);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);
    }

}

class Test implements Runnable{
    private boolean flag;

    Test(boolean flag) {
        this.flag=flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.locka) {
                System.out.println(Thread.currentThread().getName()+"if   locka....");
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName()+"if   locka....");
                }
            }
        } else {
            synchronized (MyLock.lockb) {
                System.out.println(Thread.currentThread().getName()+"else  lockb....");
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName()+"else locka.....");
                }
            }
        }
    }
}

class MyLock{
    public static final Object locka=new Object();
    public static final Object lockb=new Object();
}

输出的结果:

Thread-1 else lockb.......

Thread-0 if   locka.......

从结果中可以看出来,当线程1拿到了b锁,所以执行了else lockb.......,而线程0拿到了a锁执行if locka

线程0接下来需要去执行第二条语句的时候由于b锁被线程1拿着所以无法执行,线程1接下来需要去执行第二条语句的时候需要locka,但是locka被线程0拥有着,所以出现了死锁的情况。


猜你喜欢

转载自blog.csdn.net/weixin_40304387/article/details/80528090